- Replaced the previous webapp with a new Mini App built using Next.js, improving performance and maintainability. - Updated the `.gitignore` to exclude Next.js build artifacts and node modules. - Revised documentation in `AGENTS.md`, `README.md`, and `architecture.md` to reflect the new Mini App structure and technology stack. - Enhanced Dockerfile to support the new build process for the Next.js application. - Updated CI workflow to build and test the Next.js application. - Added new configuration options for the Mini App, including `MINI_APP_SHORT_NAME` for improved deep linking. - Refactored frontend testing setup to accommodate the new structure and testing framework. - Removed legacy webapp files and dependencies to streamline the project.
81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
/**
|
||
* Single duty row: event type label, name, time range.
|
||
* Used inside timeline cards and day detail. Ported from webapp/js/dutyList.js dutyItemHtml.
|
||
*/
|
||
|
||
"use client";
|
||
|
||
import { useTranslation } from "@/i18n/use-translation";
|
||
import { formatHHMM, formatDateKey } from "@/lib/date-utils";
|
||
import { cn } from "@/lib/utils";
|
||
import type { DutyWithUser } from "@/types";
|
||
|
||
export interface DutyItemProps {
|
||
duty: DutyWithUser;
|
||
/** Override type label (e.g. "On duty now"). */
|
||
typeLabelOverride?: string;
|
||
/** Show "until HH:MM" instead of full range (for current duty). */
|
||
showUntilEnd?: boolean;
|
||
/** Extra class, e.g. for current duty highlight. */
|
||
isCurrent?: boolean;
|
||
className?: string;
|
||
}
|
||
|
||
const borderByType = {
|
||
duty: "border-l-duty",
|
||
unavailable: "border-l-unavailable",
|
||
vacation: "border-l-vacation",
|
||
} as const;
|
||
|
||
/**
|
||
* Renders type badge, name, and time. Timeline cards use event_type for border color.
|
||
*/
|
||
export function DutyItem({
|
||
duty,
|
||
typeLabelOverride,
|
||
showUntilEnd = false,
|
||
isCurrent = false,
|
||
className,
|
||
}: DutyItemProps) {
|
||
const { t } = useTranslation();
|
||
const typeLabel =
|
||
typeLabelOverride ?? t(`event_type.${duty.event_type || "duty"}`);
|
||
const eventType = (duty.event_type || "duty") as keyof typeof borderByType;
|
||
const borderClass = isCurrent ? "border-l-today" : borderByType[eventType] ?? "border-l-duty";
|
||
|
||
let timeOrRange: string;
|
||
if (showUntilEnd && duty.event_type === "duty") {
|
||
timeOrRange = t("duty.until", { time: formatHHMM(duty.end_at) });
|
||
} else if (duty.event_type === "vacation" || duty.event_type === "unavailable") {
|
||
const startStr = formatDateKey(duty.start_at);
|
||
const endStr = formatDateKey(duty.end_at);
|
||
timeOrRange = startStr === endStr ? startStr : `${startStr} – ${endStr}`;
|
||
} else {
|
||
timeOrRange = `${formatHHMM(duty.start_at)} – ${formatHHMM(duty.end_at)}`;
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
"grid grid-cols-1 gap-y-0.5 items-baseline rounded-lg bg-surface px-2.5 py-2",
|
||
"border-l-[3px] shadow-sm",
|
||
"min-h-0",
|
||
borderClass,
|
||
isCurrent && "bg-[var(--surface-today-tint)]",
|
||
className
|
||
)}
|
||
data-slot="duty-item"
|
||
>
|
||
<span className="text-xs text-muted col-span-1 row-start-1">
|
||
{typeLabel}
|
||
</span>
|
||
<span className="font-semibold min-w-0 col-span-1 row-start-2 col-start-1">
|
||
{duty.full_name}
|
||
</span>
|
||
<span className="text-[0.8rem] text-muted col-span-1 row-start-3 col-start-1">
|
||
{timeOrRange}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|