mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 07:36:39 +02:00
Backend: TradeEvent Prisma model, NestJS CRUD module with date validation and tenant isolation. Frontend: Admin Events page with create/edit/delete modals, dashboard countdown tiles showing upcoming/ongoing/ended events with progress bars, and useEventCountdown hook for live timer updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
import { useActiveTradeEvents } from '../crm/hooks';
|
||
import { useEventCountdown, type EventCountdown } from '../hooks/useEventCountdown';
|
||
import type { TradeEvent } from '../crm/types';
|
||
import styles from './EventCountdownTiles.module.css';
|
||
|
||
// ============================================================
|
||
// Helpers
|
||
// ============================================================
|
||
|
||
function formatDateRange(start: string, end: string): string {
|
||
const s = new Date(start).toLocaleDateString('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
});
|
||
const e = new Date(end).toLocaleDateString('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
});
|
||
return `${s} – ${e}`;
|
||
}
|
||
|
||
/** Hide events that ended more than 24h ago */
|
||
function isStillRelevant(event: TradeEvent): boolean {
|
||
const endDay = new Date(event.endDate);
|
||
endDay.setHours(23, 59, 59, 999);
|
||
const cutoff = new Date(endDay.getTime() + 24 * 60 * 60 * 1000);
|
||
return new Date() < cutoff;
|
||
}
|
||
|
||
// ============================================================
|
||
// Single Tile
|
||
// ============================================================
|
||
|
||
function CountdownTile({ event }: { event: TradeEvent }) {
|
||
const countdown: EventCountdown = useEventCountdown(event);
|
||
|
||
const borderColor =
|
||
countdown.status === 'upcoming'
|
||
? '#3b82f6'
|
||
: countdown.status === 'ongoing'
|
||
? '#22c55e'
|
||
: '#9ca3af';
|
||
|
||
return (
|
||
<div className={styles.tile} style={{ borderLeftColor: borderColor }}>
|
||
<div className={styles.tileHeader}>
|
||
<h3 className={styles.tileName}>{event.name}</h3>
|
||
<span
|
||
className={`${styles.statusChip} ${styles[`chip_${countdown.status}`]}`}
|
||
>
|
||
{countdown.status === 'upcoming'
|
||
? 'Bevorstehend'
|
||
: countdown.status === 'ongoing'
|
||
? 'Laeuft'
|
||
: 'Beendet'}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Countdown / Progress */}
|
||
<div className={styles.countdownRow}>
|
||
{countdown.status === 'upcoming' && (
|
||
<span className={styles.countdownLabel}>{countdown.label}</span>
|
||
)}
|
||
{countdown.status === 'ongoing' && (
|
||
<>
|
||
<span className={styles.countdownLabel}>
|
||
{countdown.label} — laeuft!
|
||
</span>
|
||
<div className={styles.progressBar}>
|
||
<div
|
||
className={styles.progressFill}
|
||
style={{ width: `${countdown.progressPercent}%` }}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
{countdown.status === 'ended' && (
|
||
<span className={styles.countdownLabelMuted}>Beendet</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Meta Info */}
|
||
<div className={styles.tileMeta}>
|
||
<div className={styles.metaItem}>
|
||
<svg
|
||
width="12"
|
||
height="12"
|
||
viewBox="0 0 16 16"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="1.5"
|
||
strokeLinecap="round"
|
||
>
|
||
<rect x="2" y="3" width="12" height="11" rx="1" />
|
||
<path d="M5 1v4M11 1v4M2 7h12" />
|
||
</svg>
|
||
<span>{formatDateRange(event.startDate, event.endDate)}</span>
|
||
</div>
|
||
|
||
{event.location && (
|
||
<div className={styles.metaItem}>
|
||
<svg
|
||
width="12"
|
||
height="12"
|
||
viewBox="0 0 16 16"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="1.5"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
>
|
||
<path d="M8 1C5.2 1 3 3.2 3 6c0 4 5 9 5 9s5-5 5-9c0-2.8-2.2-5-5-5z" />
|
||
<circle cx="8" cy="6" r="1.5" />
|
||
</svg>
|
||
<span>
|
||
{event.location}
|
||
{event.boothInfo && ` · ${event.boothInfo}`}
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{!event.location && event.boothInfo && (
|
||
<div className={styles.metaItem}>
|
||
<svg
|
||
width="12"
|
||
height="12"
|
||
viewBox="0 0 16 16"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="1.5"
|
||
strokeLinecap="round"
|
||
>
|
||
<rect x="1" y="4" width="14" height="10" rx="1" />
|
||
<path d="M1 7h14" />
|
||
</svg>
|
||
<span>{event.boothInfo}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{event.websiteUrl && (
|
||
<a
|
||
href={event.websiteUrl}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className={styles.websiteLink}
|
||
>
|
||
<svg
|
||
width="12"
|
||
height="12"
|
||
viewBox="0 0 16 16"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="1.5"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
>
|
||
<path d="M6 3H3a1 1 0 00-1 1v9a1 1 0 001 1h9a1 1 0 001-1v-3M10 2h4v4M7 9l7-7" />
|
||
</svg>
|
||
Website
|
||
</a>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// EventCountdownTiles — Grid
|
||
// ============================================================
|
||
|
||
export function EventCountdownTiles() {
|
||
const { data, isLoading } = useActiveTradeEvents();
|
||
const events = (data?.data ?? []).filter(isStillRelevant);
|
||
|
||
if (isLoading || events.length === 0) return null;
|
||
|
||
return (
|
||
<div className={styles.grid}>
|
||
{events.map((ev) => (
|
||
<CountdownTile key={ev.id} event={ev} />
|
||
))}
|
||
</div>
|
||
);
|
||
}
|