updated
This commit is contained in:
parent
73607d2605
commit
5e6f7e872d
@ -1,14 +1,177 @@
|
|||||||
// app/(app)/dashboard/page.tsx
|
// app/(app)/dashboard/page.tsx
|
||||||
|
import Alerts from '@/components/ui/Alerts';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
const dtf = new Intl.DateTimeFormat('de-DE', {
|
||||||
|
dateStyle: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function DashboardPage() {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Start / Ende des heutigen Tages (ohne Uhrzeit)
|
||||||
|
const startOfToday = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate(),
|
||||||
|
);
|
||||||
|
const startOfTomorrow = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate() + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔴 Überfällige Geräte: loanedUntil < heute
|
||||||
|
const overdueDevices = await prisma.device.findMany({
|
||||||
|
where: {
|
||||||
|
loanedTo: { not: null },
|
||||||
|
loanedUntil: { lt: startOfToday },
|
||||||
|
},
|
||||||
|
orderBy: { loanedUntil: 'asc' },
|
||||||
|
select: {
|
||||||
|
inventoryNumber: true,
|
||||||
|
name: true,
|
||||||
|
loanedTo: true,
|
||||||
|
loanedUntil: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🟡 Heute fällige Geräte: loanedUntil am heutigen Tag
|
||||||
|
const dueTodayDevices = await prisma.device.findMany({
|
||||||
|
where: {
|
||||||
|
loanedTo: { not: null },
|
||||||
|
loanedUntil: {
|
||||||
|
gte: startOfToday,
|
||||||
|
lt: startOfTomorrow,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { loanedUntil: 'asc' },
|
||||||
|
select: {
|
||||||
|
inventoryNumber: true,
|
||||||
|
name: true,
|
||||||
|
loanedTo: true,
|
||||||
|
loanedUntil: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasOverdue = overdueDevices.length > 0;
|
||||||
|
const hasDueToday = dueTodayDevices.length > 0;
|
||||||
|
|
||||||
export default function DashboardPage() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
{/* 🔴 Überfällige Geräte (rot) */}
|
||||||
Geräte-Inventar
|
{hasOverdue && (
|
||||||
</h1>
|
<div className="mb-4">
|
||||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
<Alerts
|
||||||
Hier könntest du gleich als Nächstes eine Übersicht deiner Geräte einbauen.
|
tone="error"
|
||||||
</p>
|
title={
|
||||||
|
overdueDevices.length === 1
|
||||||
|
? 'Es gibt ein überfälliges Gerät'
|
||||||
|
: `Es gibt ${overdueDevices.length} überfällige Geräte`
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Diese Geräte haben das Rückgabedatum bereits überschritten:</p>
|
||||||
|
<ul className="list-disc space-y-1 pl-5 text-sm">
|
||||||
|
{overdueDevices.map((d) => (
|
||||||
|
<li key={d.inventoryNumber}>
|
||||||
|
<span className="font-mono">
|
||||||
|
{d.inventoryNumber}
|
||||||
|
</span>
|
||||||
|
{d.name && (
|
||||||
|
<>
|
||||||
|
{' – '}
|
||||||
|
<a
|
||||||
|
href={`/devices?device=${encodeURIComponent(
|
||||||
|
d.inventoryNumber,
|
||||||
|
)}`}
|
||||||
|
className="font-medium underline text-red-800 hover:text-red-700 dark:text-red-200 dark:hover:text-red-100"
|
||||||
|
>
|
||||||
|
{d.name}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{d.loanedTo && <span> (an {d.loanedTo})</span>}
|
||||||
|
{d.loanedUntil && (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
– fällig am {dtf.format(d.loanedUntil)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightContent={
|
||||||
|
<a
|
||||||
|
href="/devices"
|
||||||
|
className="font-medium whitespace-nowrap text-red-800 hover:text-red-700 dark:text-red-200 dark:hover:text-red-100"
|
||||||
|
>
|
||||||
|
Zur Geräteliste
|
||||||
|
<span aria-hidden="true"> →</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 🟡 Heute fällige Geräte (gelb) */}
|
||||||
|
{hasDueToday && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Alerts
|
||||||
|
tone="warning"
|
||||||
|
title={
|
||||||
|
dueTodayDevices.length === 1
|
||||||
|
? 'Ein Gerät ist heute fällig'
|
||||||
|
: `${dueTodayDevices.length} Geräte sind heute fällig`
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Diese Geräte sollten heute zurückgegeben werden:</p>
|
||||||
|
<ul className="list-disc space-y-1 pl-5 text-sm">
|
||||||
|
{dueTodayDevices.map((d) => (
|
||||||
|
<li key={d.inventoryNumber}>
|
||||||
|
<span className="font-mono">
|
||||||
|
{d.inventoryNumber}
|
||||||
|
</span>
|
||||||
|
{d.name && (
|
||||||
|
<>
|
||||||
|
{' – '}
|
||||||
|
<a
|
||||||
|
href={`/devices?device=${encodeURIComponent(
|
||||||
|
d.inventoryNumber,
|
||||||
|
)}`}
|
||||||
|
className="font-medium underline text-yellow-800 hover:text-yellow-700 dark:text-yellow-200 dark:hover:text-yellow-100"
|
||||||
|
>
|
||||||
|
{d.name}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{d.loanedTo && <span> (an {d.loanedTo})</span>}
|
||||||
|
{d.loanedUntil && (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
– fällig am {dtf.format(d.loanedUntil)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightContent={
|
||||||
|
<a
|
||||||
|
href="/devices"
|
||||||
|
className="font-medium whitespace-nowrap text-yellow-800 hover:text-yellow-700 dark:text-yellow-200 dark:hover:text-yellow-100"
|
||||||
|
>
|
||||||
|
Zur Geräteliste
|
||||||
|
<span aria-hidden="true"> →</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,8 +44,6 @@ type DeviceDetailsGridProps = {
|
|||||||
function DeviceDetailsGrid({
|
function DeviceDetailsGrid({
|
||||||
device,
|
device,
|
||||||
onStartLoan,
|
onStartLoan,
|
||||||
canEdit,
|
|
||||||
onEdit,
|
|
||||||
}: DeviceDetailsGridProps) {
|
}: DeviceDetailsGridProps) {
|
||||||
|
|
||||||
const [activeSection, setActiveSection] =
|
const [activeSection, setActiveSection] =
|
||||||
@ -114,6 +112,7 @@ function DeviceDetailsGrid({
|
|||||||
{ id: 'info', label: 'Stammdaten' },
|
{ id: 'info', label: 'Stammdaten' },
|
||||||
{ id: 'zubehoer', label: 'Zubehör' },
|
{ id: 'zubehoer', label: 'Zubehör' },
|
||||||
]}
|
]}
|
||||||
|
variant='pillsBrand'
|
||||||
value={activeSection}
|
value={activeSection}
|
||||||
onChange={(id) =>
|
onChange={(id) =>
|
||||||
setActiveSection(id as 'info' | 'zubehoer')
|
setActiveSection(id as 'info' | 'zubehoer')
|
||||||
@ -142,9 +141,11 @@ function DeviceDetailsGrid({
|
|||||||
Status
|
Status
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
<div className="mt-2 space-y-2">
|
||||||
{/* linke „Spalte“: nur inhaltsbreit */}
|
{/* Zeile 1: Badge + Buttons nebeneinander */}
|
||||||
<div className="flex w-auto shrink-0 flex-col gap-1">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
{/* Badge */}
|
||||||
|
<div className="flex w-auto shrink-0">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex w-fit items-center gap-2 rounded-full px-3 py-1 text-xs font-medium ${statusClasses}`}
|
className={`inline-flex w-fit items-center gap-2 rounded-full px-3 py-1 text-xs font-medium ${statusClasses}`}
|
||||||
>
|
>
|
||||||
@ -153,35 +154,6 @@ function DeviceDetailsGrid({
|
|||||||
/>
|
/>
|
||||||
<span>{statusLabel}</span>
|
<span>{statusLabel}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{device.loanedTo && (
|
|
||||||
<span className="text-xs text-gray-700 dark:text-gray-200">
|
|
||||||
an{' '}
|
|
||||||
<span className="font-semibold">
|
|
||||||
{device.loanedTo}
|
|
||||||
</span>
|
|
||||||
{device.loanedFrom && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
seit{' '}
|
|
||||||
{dtf.format(new Date(device.loanedFrom))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{device.loanedUntil && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
bis{' '}
|
|
||||||
{dtf.format(new Date(device.loanedUntil))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{device.loanComment && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
- Hinweis: {device.loanComment}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* rechte Seite: Buttons */}
|
{/* rechte Seite: Buttons */}
|
||||||
@ -193,19 +165,36 @@ function DeviceDetailsGrid({
|
|||||||
>
|
>
|
||||||
{isLoaned ? 'Verleih bearbeiten' : 'Gerät verleihen'}
|
{isLoaned ? 'Verleih bearbeiten' : 'Gerät verleihen'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{canEdit && onEdit && (
|
|
||||||
<Button
|
|
||||||
size="md"
|
|
||||||
variant="soft"
|
|
||||||
tone="indigo"
|
|
||||||
onClick={onEdit}
|
|
||||||
>
|
|
||||||
Bearbeiten
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Zeile 2: Verleih-Details über volle Breite */}
|
||||||
|
{device.loanedTo && (
|
||||||
|
<p className="text-xs text-gray-700 dark:text-gray-200">
|
||||||
|
an{' '}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{device.loanedTo}
|
||||||
|
</span>
|
||||||
|
{device.loanedFrom && (
|
||||||
|
<>
|
||||||
|
{' '}seit{' '}
|
||||||
|
{dtf.format(new Date(device.loanedFrom))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{device.loanedUntil && (
|
||||||
|
<>
|
||||||
|
{' '}bis{' '}
|
||||||
|
{dtf.format(new Date(device.loanedUntil))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{device.loanComment && (
|
||||||
|
<>
|
||||||
|
{' '}– Hinweis: {device.loanComment}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trenner nach Verleihstatus */}
|
{/* Trenner nach Verleihstatus */}
|
||||||
@ -531,17 +520,35 @@ export default function DeviceDetailModal({
|
|||||||
}}
|
}}
|
||||||
headerExtras={
|
headerExtras={
|
||||||
device && (
|
device && (
|
||||||
|
<div className="flex items-center justify-between gap-3 sm:justify-end">
|
||||||
|
{/* Mobile: Tabs im Header */}
|
||||||
<div className="sm:hidden">
|
<div className="sm:hidden">
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={[
|
tabs={[
|
||||||
{ id: 'details', label: 'Details' },
|
{ id: 'details', label: 'Details' },
|
||||||
{ id: 'history', label: 'Änderungsverlauf' },
|
{ id: 'history', label: 'Änderungsverlauf' },
|
||||||
]}
|
]}
|
||||||
|
variant='pillsBrand'
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={(id) => setActiveTab(id as 'details' | 'history')}
|
onChange={(id) =>
|
||||||
|
setActiveTab(id as 'details' | 'history')
|
||||||
|
}
|
||||||
ariaLabel="Ansicht wählen"
|
ariaLabel="Ansicht wählen"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Rechts: Bearbeiten-Button nur wenn erlaubt */}
|
||||||
|
{canEdit && onEdit && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="soft"
|
||||||
|
tone="indigo"
|
||||||
|
onClick={() => onEdit(device.inventoryNumber)}
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sidebar={
|
sidebar={
|
||||||
@ -550,14 +557,14 @@ export default function DeviceDetailModal({
|
|||||||
{/* QR-Code oben, nicht scrollend */}
|
{/* QR-Code oben, nicht scrollend */}
|
||||||
<div className="rounded-lg border border-gray-800 bg-gray-900/70 px-4 py-3 shadow-sm">
|
<div className="rounded-lg border border-gray-800 bg-gray-900/70 px-4 py-3 shadow-sm">
|
||||||
<div className="mt-2 flex justify-center">
|
<div className="mt-2 flex justify-center">
|
||||||
<div className="rounded-md bg-black/80 p-2">
|
<div className="rounded-md bg-black/80 px-3 py-3 flex flex-col items-center gap-2">
|
||||||
<DeviceQrCode inventoryNumber={device.inventoryNumber} />
|
<DeviceQrCode inventoryNumber={device.inventoryNumber} />
|
||||||
</div>
|
<p className="text-[13px] font-mono tracking-wide text-gray-100">
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-center text-[14px] text-gray-500">
|
|
||||||
{device.inventoryNumber}
|
{device.inventoryNumber}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-800 dark:border-white/10 mx-1" />
|
<div className="border-t border-gray-800 dark:border-white/10 mx-1" />
|
||||||
|
|
||||||
@ -606,7 +613,7 @@ export default function DeviceDetailModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop-Inhalt links: nur Details, Verlauf rechts in sidebar */}
|
{/* Desktop */}
|
||||||
<div className="hidden sm:block pr-2">
|
<div className="hidden sm:block pr-2">
|
||||||
<DeviceDetailsGrid
|
<DeviceDetailsGrid
|
||||||
device={device}
|
device={device}
|
||||||
|
|||||||
@ -399,6 +399,7 @@ export default function DeviceEditModal({
|
|||||||
{ id: 'fields', label: 'Stammdaten' },
|
{ id: 'fields', label: 'Stammdaten' },
|
||||||
{ id: 'relations', label: 'Zubehör' },
|
{ id: 'relations', label: 'Zubehör' },
|
||||||
]}
|
]}
|
||||||
|
variant='pillsBrand'
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={(id) => setActiveTab(id as 'fields' | 'relations')}
|
onChange={(id) => setActiveTab(id as 'fields' | 'relations')}
|
||||||
ariaLabel="Bearbeitungsansicht wählen"
|
ariaLabel="Bearbeitungsansicht wählen"
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
// app/(app)/devices/page.tsx
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import Button from '@/components/ui/Button';
|
import Button from '@/components/ui/Button';
|
||||||
import Table, { TableColumn } from '@/components/ui/Table';
|
import Table, { TableColumn } from '@/components/ui/Table';
|
||||||
import { Dropdown } from '@/components/ui/Dropdown';
|
import { Dropdown } from '@/components/ui/Dropdown';
|
||||||
@ -18,6 +18,7 @@ import type { TagOption } from '@/components/ui/TagMultiCombobox';
|
|||||||
import DeviceEditModal from './DeviceEditModal';
|
import DeviceEditModal from './DeviceEditModal';
|
||||||
import DeviceDetailModal from './DeviceDetailModal';
|
import DeviceDetailModal from './DeviceDetailModal';
|
||||||
import DeviceCreateModal from './DeviceCreateModal';
|
import DeviceCreateModal from './DeviceCreateModal';
|
||||||
|
import Badge from '@/components/ui/Badge';
|
||||||
|
|
||||||
export type AccessorySummary = {
|
export type AccessorySummary = {
|
||||||
inventoryNumber: string;
|
inventoryNumber: string;
|
||||||
@ -54,8 +55,11 @@ export type DeviceDetail = {
|
|||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PrimaryTab = 'main' | 'accessories' | 'all';
|
||||||
|
type StatusTab = 'all' | 'loaned' | 'dueToday' | 'overdue';
|
||||||
|
|
||||||
function formatDate(iso: string | null | undefined) {
|
function formatDate(iso: string | null | undefined) {
|
||||||
if (!iso) return '–'; // oder '' wenn du es leer willst
|
if (!iso) return '–';
|
||||||
|
|
||||||
return new Intl.DateTimeFormat('de-DE', {
|
return new Intl.DateTimeFormat('de-DE', {
|
||||||
dateStyle: 'short',
|
dateStyle: 'short',
|
||||||
@ -125,8 +129,28 @@ const columns: TableColumn<DeviceDetail>[] = [
|
|||||||
header: 'Tags',
|
header: 'Tags',
|
||||||
sortable: false,
|
sortable: false,
|
||||||
canHide: true,
|
canHide: true,
|
||||||
render: (row) =>
|
cellClassName: 'whitespace-normal max-w-xs',
|
||||||
row.tags && row.tags.length > 0 ? row.tags.join(', ') : '',
|
render: (row) => {
|
||||||
|
const tags = row.tags ?? [];
|
||||||
|
|
||||||
|
if (!tags.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<Badge
|
||||||
|
key={tag}
|
||||||
|
size="sm"
|
||||||
|
tone="indigo"
|
||||||
|
variant="flat"
|
||||||
|
shape="pill"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'updatedAt',
|
key: 'updatedAt',
|
||||||
@ -138,6 +162,8 @@ const columns: TableColumn<DeviceDetail>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function DevicesPage() {
|
export default function DevicesPage() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const [devices, setDevices] = useState<DeviceDetail[]>([]);
|
const [devices, setDevices] = useState<DeviceDetail[]>([]);
|
||||||
const [listLoading, setListLoading] = useState(false);
|
const [listLoading, setListLoading] = useState(false);
|
||||||
const [listError, setListError] = useState<string | null>(null);
|
const [listError, setListError] = useState<string | null>(null);
|
||||||
@ -151,21 +177,15 @@ export default function DevicesPage() {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// TODO: Ersetze das durch deinen echten User-/Gruppen-Mechanismus
|
|
||||||
// Beispiel: aktuelle Benutzergruppen (z.B. aus Context oder eigenem Hook)
|
|
||||||
const currentUserGroups: string[] = []; // Platzhalter
|
|
||||||
|
|
||||||
// Nur User in dieser Gruppe sollen Geräte bearbeiten dürfen
|
// Nur User in dieser Gruppe sollen Geräte bearbeiten dürfen
|
||||||
const canEditDevices = currentUserGroups.includes('INVENTAR_ADMIN');
|
const canEditDevices = Boolean(
|
||||||
|
(session?.user as any)?.groupCanEditDevices,
|
||||||
|
);
|
||||||
|
|
||||||
// 🔹 Tab-Filter: Hauptgeräte / Zubehör / Alle
|
// 🔹 Oberste Tabs: Hauptgeräte / Zubehör / Alle Geräte
|
||||||
const [activeTab, setActiveTab] =
|
const [primaryTab, setPrimaryTab] = useState<PrimaryTab>('all');
|
||||||
useState<'main' | 'accessories' | 'all'>('main');
|
// 🔹 Untere Tabs: Leihstatus
|
||||||
|
const [statusTab, setStatusTab] = useState<StatusTab>('all');
|
||||||
// 🔹 Counters für Badges
|
|
||||||
const mainCount = devices.filter((d) => !d.parentInventoryNumber).length;
|
|
||||||
const accessoriesCount = devices.filter((d) => !!d.parentInventoryNumber).length;
|
|
||||||
const allCount = devices.length;
|
|
||||||
|
|
||||||
/* ───────── Geräte-Liste laden ───────── */
|
/* ───────── Geräte-Liste laden ───────── */
|
||||||
|
|
||||||
@ -210,7 +230,7 @@ export default function DevicesPage() {
|
|||||||
}, [loadDevices]);
|
}, [loadDevices]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchParams) return; // TS happy
|
if (!searchParams) return;
|
||||||
|
|
||||||
const fromDevice = searchParams.get('device');
|
const fromDevice = searchParams.get('device');
|
||||||
const fromInventory =
|
const fromInventory =
|
||||||
@ -317,8 +337,6 @@ export default function DevicesPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimistisch aus lokaler Liste entfernen
|
|
||||||
// (zusätzlich kommt noch der Socket-Event device:deleted)
|
|
||||||
setDevices((prev) =>
|
setDevices((prev) =>
|
||||||
prev.filter((d) => d.inventoryNumber !== inventoryNumber),
|
prev.filter((d) => d.inventoryNumber !== inventoryNumber),
|
||||||
);
|
);
|
||||||
@ -346,15 +364,12 @@ export default function DevicesPage() {
|
|||||||
setDetailInventoryNumber(null);
|
setDetailInventoryNumber(null);
|
||||||
|
|
||||||
if (!searchParams) {
|
if (!searchParams) {
|
||||||
// Fallback: einfach auf /devices ohne Query
|
|
||||||
router.replace('/devices', { scroll: false });
|
router.replace('/devices', { scroll: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadonlyURLSearchParams → string → URLSearchParams kopieren
|
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
|
||||||
// alle möglichen Detail-Parameter entfernen
|
|
||||||
params.delete('device');
|
params.delete('device');
|
||||||
params.delete('inventoryNumber');
|
params.delete('inventoryNumber');
|
||||||
params.delete('inv');
|
params.delete('inv');
|
||||||
@ -367,28 +382,78 @@ export default function DevicesPage() {
|
|||||||
|
|
||||||
const handleEditFromDetail = useCallback(
|
const handleEditFromDetail = useCallback(
|
||||||
(inventoryNumber: string) => {
|
(inventoryNumber: string) => {
|
||||||
// Detail-Modal schließen + URL /device-Query aufräumen
|
|
||||||
closeDetailModal();
|
closeDetailModal();
|
||||||
// danach Edit-Modal öffnen
|
|
||||||
setEditInventoryNumber(inventoryNumber);
|
setEditInventoryNumber(inventoryNumber);
|
||||||
},
|
},
|
||||||
[closeDetailModal],
|
[closeDetailModal],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* ───────── Counter & Filter ───────── */
|
||||||
|
|
||||||
/* ───────── Filter nach Tab ───────── */
|
// Tag-Grenzen für "heute"
|
||||||
|
const todayStart = new Date();
|
||||||
|
todayStart.setHours(0, 0, 0, 0);
|
||||||
|
const tomorrowStart = new Date(todayStart);
|
||||||
|
tomorrowStart.setDate(tomorrowStart.getDate() + 1);
|
||||||
|
|
||||||
const filteredDevices = devices.filter((d) => {
|
// Counts für oberste Tabs (immer über alle Geräte)
|
||||||
if (activeTab === 'main') {
|
const mainCount = devices.filter((d) => !d.parentInventoryNumber).length;
|
||||||
// Hauptgeräte: kein parent → eigenständig
|
const accessoriesCount = devices.filter((d) => !!d.parentInventoryNumber).length;
|
||||||
return !d.parentInventoryNumber;
|
const allCount = devices.length;
|
||||||
}
|
|
||||||
if (activeTab === 'accessories') {
|
// Zuerst nach primaryTab filtern → Basis-Menge für Status-Tabs
|
||||||
// Zubehör: hat ein Hauptgerät
|
const baseDevices = devices.filter((d) => {
|
||||||
return !!d.parentInventoryNumber;
|
const hasParent = !!d.parentInventoryNumber;
|
||||||
}
|
switch (primaryTab) {
|
||||||
// "all"
|
case 'main':
|
||||||
|
return !hasParent;
|
||||||
|
case 'accessories':
|
||||||
|
return hasParent;
|
||||||
|
case 'all':
|
||||||
|
default:
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Counts für Status-Tabs (abhängig vom gewählten primaryTab)
|
||||||
|
const loanedCount = baseDevices.filter((d) => !!d.loanedTo).length;
|
||||||
|
const overdueCount = baseDevices.filter((d) => {
|
||||||
|
if (!d.loanedTo || !d.loanedUntil) return false;
|
||||||
|
const until = new Date(d.loanedUntil);
|
||||||
|
return until < todayStart;
|
||||||
|
}).length;
|
||||||
|
const dueTodayCount = baseDevices.filter((d) => {
|
||||||
|
if (!d.loanedTo || !d.loanedUntil) return false;
|
||||||
|
const until = new Date(d.loanedUntil);
|
||||||
|
return until >= todayStart && until < tomorrowStart;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
// Endgültige Filterung nach StatusTab
|
||||||
|
const filteredDevices = baseDevices.filter((d) => {
|
||||||
|
const isLoaned = !!d.loanedTo;
|
||||||
|
const until = d.loanedUntil ? new Date(d.loanedUntil) : null;
|
||||||
|
|
||||||
|
switch (statusTab) {
|
||||||
|
case 'all':
|
||||||
|
return true;
|
||||||
|
case 'loaned':
|
||||||
|
return isLoaned;
|
||||||
|
case 'overdue':
|
||||||
|
return (
|
||||||
|
isLoaned &&
|
||||||
|
!!until &&
|
||||||
|
until < todayStart
|
||||||
|
);
|
||||||
|
case 'dueToday':
|
||||||
|
return (
|
||||||
|
isLoaned &&
|
||||||
|
!!until &&
|
||||||
|
until >= todayStart &&
|
||||||
|
until < tomorrowStart
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ───────── Render ───────── */
|
/* ───────── Render ───────── */
|
||||||
@ -421,10 +486,17 @@ export default function DevicesPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🔹 Tabs für Hauptgeräte/Zubehör/Alle */}
|
{/* 🔹 Tabs: oben Gerätetyp, darunter Leihstatus */}
|
||||||
<div className="mt-6">
|
<div className="mt-6 space-y-3">
|
||||||
|
{/* Oberste Ebene */}
|
||||||
<Tabs
|
<Tabs
|
||||||
|
variant='pillsBrand'
|
||||||
tabs={[
|
tabs={[
|
||||||
|
{
|
||||||
|
id: 'all',
|
||||||
|
label: 'Alle Geräte',
|
||||||
|
count: allCount,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'main',
|
id: 'main',
|
||||||
label: 'Hauptgeräte',
|
label: 'Hauptgeräte',
|
||||||
@ -435,26 +507,43 @@ export default function DevicesPage() {
|
|||||||
label: 'Zubehör',
|
label: 'Zubehör',
|
||||||
count: accessoriesCount,
|
count: accessoriesCount,
|
||||||
},
|
},
|
||||||
|
]}
|
||||||
|
value={primaryTab}
|
||||||
|
onChange={(id) => setPrimaryTab(id as PrimaryTab)}
|
||||||
|
ariaLabel="Geräte-Typ filtern"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Untere Ebene: Leihstatus (abhängig von primaryTab, Counts basieren auf baseDevices) */}
|
||||||
|
<Tabs
|
||||||
|
variant='pillsBrand'
|
||||||
|
tabs={[
|
||||||
{
|
{
|
||||||
id: 'all',
|
id: 'all',
|
||||||
label: 'Alle Geräte',
|
label: 'Alle',
|
||||||
count: allCount,
|
count: baseDevices.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'loaned',
|
||||||
|
label: 'Verliehen',
|
||||||
|
count: loanedCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dueToday',
|
||||||
|
label: 'Heute fällig',
|
||||||
|
count: dueTodayCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'overdue',
|
||||||
|
label: 'Überfällig',
|
||||||
|
count: overdueCount,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={activeTab}
|
value={statusTab}
|
||||||
onChange={(id) =>
|
onChange={(id) => setStatusTab(id as StatusTab)}
|
||||||
setActiveTab(id as 'main' | 'accessories' | 'all')
|
ariaLabel="Leihstatus filtern"
|
||||||
}
|
|
||||||
ariaLabel="Geräteliste filtern"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{listLoading && (
|
|
||||||
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Geräte werden geladen …
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{listError && (
|
{listError && (
|
||||||
<p className="mt-4 text-sm text-red-600 dark:text-red-400">
|
<p className="mt-4 text-sm text-red-600 dark:text-red-400">
|
||||||
{listError}
|
{listError}
|
||||||
@ -469,6 +558,7 @@ export default function DevicesPage() {
|
|||||||
getRowId={(row) => row.inventoryNumber}
|
getRowId={(row) => row.inventoryNumber}
|
||||||
selectable
|
selectable
|
||||||
actionsHeader=""
|
actionsHeader=""
|
||||||
|
isLoading={listLoading}
|
||||||
renderActions={(row) => (
|
renderActions={(row) => (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
{/* Desktop: drei Icon-Buttons nebeneinander */}
|
{/* Desktop: drei Icon-Buttons nebeneinander */}
|
||||||
@ -482,6 +572,7 @@ export default function DevicesPage() {
|
|||||||
onClick={() => handleDetails(row.inventoryNumber)}
|
onClick={() => handleDetails(row.inventoryNumber)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{canEditDevices && (
|
||||||
<Button
|
<Button
|
||||||
variant="soft"
|
variant="soft"
|
||||||
tone="gray"
|
tone="gray"
|
||||||
@ -490,6 +581,7 @@ export default function DevicesPage() {
|
|||||||
aria-label={`Gerät ${row.inventoryNumber} bearbeiten`}
|
aria-label={`Gerät ${row.inventoryNumber} bearbeiten`}
|
||||||
onClick={() => handleEdit(row.inventoryNumber)}
|
onClick={() => handleEdit(row.inventoryNumber)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="soft"
|
variant="soft"
|
||||||
|
|||||||
@ -45,11 +45,6 @@ const navigation = [
|
|||||||
{ name: 'Personen', href: '/users', icon: UserIcon },
|
{ name: 'Personen', href: '/users', icon: UserIcon },
|
||||||
];
|
];
|
||||||
|
|
||||||
const userNavigation = [
|
|
||||||
{ name: 'Your profile', href: '#' },
|
|
||||||
{ name: 'Abmelden', href: '#' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function classNames(...classes: Array<string | boolean | null | undefined>) {
|
function classNames(...classes: Array<string | boolean | null | undefined>) {
|
||||||
return classes.filter(Boolean).join(' ');
|
return classes.filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
@ -72,7 +67,12 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const displayName = rawName;
|
const displayName = rawName;
|
||||||
const avatarName = rawName;
|
const avatarName = rawName;
|
||||||
const avatarUrl = session?.user?.image ?? null;
|
|
||||||
|
// Avatar-URL bevorzugt aus avatarUrl, sonst Fallback auf image
|
||||||
|
const avatarUrl =
|
||||||
|
status === 'authenticated'
|
||||||
|
? ((session?.user as any).avatarUrl ?? session?.user?.image ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
const handleScanResult = (code: string) => {
|
const handleScanResult = (code: string) => {
|
||||||
const trimmed = code.trim();
|
const trimmed = code.trim();
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
// app/(app)/users/EditUserModal.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
|
import Switch from '@/components/ui/Switch'; // 👈 Neu
|
||||||
import type { UserWithAvatar } from './types';
|
import type { UserWithAvatar } from './types';
|
||||||
|
|
||||||
type EditUserModalProps = {
|
type EditUserModalProps = {
|
||||||
@ -15,6 +17,8 @@ type EditUserModalProps = {
|
|||||||
onLastNameChange: (value: string) => void;
|
onLastNameChange: (value: string) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
|
/** Abgeleitet aus der Gruppe: darf dieser Benutzer Geräte bearbeiten? */
|
||||||
|
canEditDevices: boolean; // 👈 Neu
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EditUserModal({
|
export default function EditUserModal({
|
||||||
@ -29,6 +33,7 @@ export default function EditUserModal({
|
|||||||
onLastNameChange,
|
onLastNameChange,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
canEditDevices, // 👈 Neu
|
||||||
}: EditUserModalProps) {
|
}: EditUserModalProps) {
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
@ -56,7 +61,7 @@ export default function EditUserModal({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSubmit();
|
onSubmit();
|
||||||
}}
|
}}
|
||||||
className="space-y-3 text-sm"
|
className="space-y-4 text-sm"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||||
@ -109,6 +114,27 @@ export default function EditUserModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 🔹 Info: Darf Geräte bearbeiten (über Gruppe gesteuert) */}
|
||||||
|
<div className="mt-2 flex items-center justify-between gap-3">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Darf Geräte bearbeiten
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
Dieser Status wird durch die zugewiesene Gruppe gesteuert.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
id="user-can-edit-devices"
|
||||||
|
name="user-can-edit-devices"
|
||||||
|
checked={canEditDevices}
|
||||||
|
onChange={() => { /* read-only, wird durch Gruppe bestimmt */ }}
|
||||||
|
disabled
|
||||||
|
ariaLabel="Benutzer darf Geräte bearbeiten (über Gruppe gesteuert)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -684,6 +684,7 @@ export default function UsersTablesClient({
|
|||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={mainTabs}
|
tabs={mainTabs}
|
||||||
|
variant='pillsBrand'
|
||||||
value={safeActiveMainTab}
|
value={safeActiveMainTab}
|
||||||
onChange={setActiveMainTab}
|
onChange={setActiveMainTab}
|
||||||
ariaLabel="Usergruppen (Cluster) auswählen"
|
ariaLabel="Usergruppen (Cluster) auswählen"
|
||||||
@ -710,6 +711,7 @@ export default function UsersTablesClient({
|
|||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={subTabs}
|
tabs={subTabs}
|
||||||
|
variant='pillsBrand'
|
||||||
value={safeActiveSubTab}
|
value={safeActiveSubTab}
|
||||||
onChange={setActiveSubTab}
|
onChange={setActiveSubTab}
|
||||||
ariaLabel="Untergruppen auswählen"
|
ariaLabel="Untergruppen auswählen"
|
||||||
@ -803,6 +805,10 @@ export default function UsersTablesClient({
|
|||||||
onLastNameChange={setEditLastName}
|
onLastNameChange={setEditLastName}
|
||||||
onClose={() => setEditUser(null)}
|
onClose={() => setEditUser(null)}
|
||||||
onSubmit={handleSaveEdit}
|
onSubmit={handleSaveEdit}
|
||||||
|
canEditDevices={(() => {
|
||||||
|
const group = allGroups.find((g) => g.id === editUser.groupId);
|
||||||
|
return !!group?.canEditDevices;
|
||||||
|
})()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
120
app/api/profile/avatar/route.ts
Normal file
120
app/api/profile/avatar/route.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
// app/api/profile/avatar/route.ts
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth-options';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
const user = session?.user as any | undefined;
|
||||||
|
const nwkennung: string | undefined = user?.nwkennung;
|
||||||
|
|
||||||
|
if (!session || !nwkennung) {
|
||||||
|
return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await req.formData();
|
||||||
|
const file = formData.get('avatar');
|
||||||
|
|
||||||
|
if (!file || !(file instanceof Blob)) {
|
||||||
|
return NextResponse.json({ error: 'NO_FILE' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||||
|
const mime = file.type || '';
|
||||||
|
|
||||||
|
if (!mime.startsWith('image/')) {
|
||||||
|
return NextResponse.json({ error: 'INVALID_TYPE' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const size: number | undefined = file.size;
|
||||||
|
if (typeof size === 'number' && size > MAX_SIZE) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'TOO_LARGE', maxSizeBytes: MAX_SIZE },
|
||||||
|
{ status: 413 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dateiendung bestimmen
|
||||||
|
let ext = '';
|
||||||
|
if ('name' in file) {
|
||||||
|
const name = (file as any).name as string;
|
||||||
|
ext = path.extname(name);
|
||||||
|
}
|
||||||
|
if (!ext) {
|
||||||
|
if (mime === 'image/jpeg') ext = '.jpg';
|
||||||
|
else if (mime === 'image/png') ext = '.png';
|
||||||
|
else if (mime === 'image/gif') ext = '.gif';
|
||||||
|
else ext = '.img';
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarsDir = path.join(process.cwd(), 'public', 'avatars');
|
||||||
|
await fs.promises.mkdir(avatarsDir, { recursive: true });
|
||||||
|
|
||||||
|
// alte Avatare löschen
|
||||||
|
const existingFiles = await fs.promises.readdir(avatarsDir);
|
||||||
|
await Promise.all(
|
||||||
|
existingFiles
|
||||||
|
.filter((f) => f.startsWith(`${nwkennung}-`))
|
||||||
|
.map((f) => fs.promises.unlink(path.join(avatarsDir, f))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Neuer, eindeutiger Dateiname
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const fileName = `${nwkennung}-${timestamp}${ext.toLowerCase()}`;
|
||||||
|
const filePath = path.join(avatarsDir, fileName);
|
||||||
|
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
await fs.promises.writeFile(filePath, buffer);
|
||||||
|
|
||||||
|
const avatarUrl = `/avatars/${fileName}`;
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { nwkennung },
|
||||||
|
data: { avatarUrl },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true, avatarUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👇 Neu: Profilbild löschen
|
||||||
|
export async function DELETE(req: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
const user = session?.user as any | undefined;
|
||||||
|
const nwkennung: string | undefined = user?.nwkennung;
|
||||||
|
|
||||||
|
if (!session || !nwkennung) {
|
||||||
|
return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarsDir = path.join(process.cwd(), 'public', 'avatars');
|
||||||
|
|
||||||
|
// Dateien löschen (falls vorhanden)
|
||||||
|
try {
|
||||||
|
const existingFiles = await fs.promises.readdir(avatarsDir);
|
||||||
|
const userFiles = existingFiles.filter((f) =>
|
||||||
|
f.startsWith(`${nwkennung}-`),
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
userFiles.map((f) => fs.promises.unlink(path.join(avatarsDir, f))),
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
// Wenn es den Ordner nicht gibt, ignorieren
|
||||||
|
if (err?.code !== 'ENOENT') {
|
||||||
|
console.error('[DELETE /api/profile/avatar] cleanup error', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// avatarUrl in DB auf null setzen
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { nwkennung },
|
||||||
|
data: { avatarUrl: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true, avatarUrl: null });
|
||||||
|
}
|
||||||
@ -1,23 +1,30 @@
|
|||||||
// /app/layout.tsx
|
// /app/layout.tsx
|
||||||
|
import type { Metadata, Viewport } from 'next';
|
||||||
import type { Metadata } from "next";
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import './globals.css';
|
||||||
import "./globals.css";
|
import Providers from './providers';
|
||||||
import Providers from "./providers";
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: '--font-geist-sans',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: '--font-geist-mono',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: 'Create Next App',
|
||||||
description: "Generated by create next app",
|
description: 'Generated by create next app',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 👇 Neu
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: 'device-width',
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 1,
|
||||||
|
userScalable: false, // verhindert Pinch-Zoom
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -26,7 +33,10 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="bg-white dark:bg-gray-950 scheme-light dark:scheme-dark">
|
<html
|
||||||
|
lang="de"
|
||||||
|
className="bg-white dark:bg-gray-950 scheme-light dark:scheme-dark"
|
||||||
|
>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased h-full overflow-x-hidden`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased h-full overflow-x-hidden`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export function DeviceQrCode({ inventoryNumber, size = 180 }: DeviceQrCodeProps)
|
|||||||
value={qrValue}
|
value={qrValue}
|
||||||
size={size}
|
size={size}
|
||||||
level="M"
|
level="M"
|
||||||
includeMargin
|
marginSize={2}
|
||||||
bgColor="#FFFFFF"
|
bgColor="#FFFFFF"
|
||||||
fgColor="#000000"
|
fgColor="#000000"
|
||||||
/>
|
/>
|
||||||
|
|||||||
184
components/ProfileAvatarModal.tsx
Normal file
184
components/ProfileAvatarModal.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
// components/ProfileAvatarModal.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Modal from '@/components/ui/Modal';
|
||||||
|
import PersonAvatar from '@/components/ui/UserAvatar';
|
||||||
|
import Button from './ui/Button';
|
||||||
|
|
||||||
|
type ProfileAvatarModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
avatarName: string;
|
||||||
|
avatarUrl?: string | null;
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn eine neue Datei gespeichert werden soll.
|
||||||
|
*/
|
||||||
|
onAvatarSelected?: (file: File) => Promise<void> | void;
|
||||||
|
/**
|
||||||
|
* Optional: wird aufgerufen, wenn der Nutzer das Profilbild löschen möchte.
|
||||||
|
* Erwartet, dass Backend + Session angepasst werden (Avatar auf null).
|
||||||
|
*/
|
||||||
|
onAvatarDelete?: () => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProfileAvatarModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
avatarName,
|
||||||
|
avatarUrl,
|
||||||
|
onAvatarSelected,
|
||||||
|
onAvatarDelete,
|
||||||
|
}: ProfileAvatarModalProps) {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// Wenn Modal geschlossen wird → State zurücksetzen
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// Preview-URL für ausgewählte Datei erzeugen
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
setPreviewUrl(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(selectedFile);
|
||||||
|
setPreviewUrl(url);
|
||||||
|
|
||||||
|
return () => URL.revokeObjectURL(url);
|
||||||
|
}, [selectedFile]);
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
setSelectedFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
if (onAvatarSelected) {
|
||||||
|
await onAvatarSelected(selectedFile);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = async () => {
|
||||||
|
if (!onAvatarDelete) return;
|
||||||
|
|
||||||
|
const sure = window.confirm('Profilbild wirklich entfernen?');
|
||||||
|
if (!sure) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
await onAvatarDelete();
|
||||||
|
// lokale Preview zurücksetzen
|
||||||
|
setSelectedFile(null);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
onClose();
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const effectiveAvatarUrl = previewUrl ?? avatarUrl ?? undefined;
|
||||||
|
const hasDeletableAvatar = !!avatarUrl; // Button nur anzeigen, wenn ein Avatar existiert
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
title="Profilbild ändern"
|
||||||
|
tone="info"
|
||||||
|
size="sm"
|
||||||
|
primaryAction={{
|
||||||
|
label: 'Speichern',
|
||||||
|
onClick: handleSave,
|
||||||
|
disabled: !selectedFile || isSaving,
|
||||||
|
}}
|
||||||
|
secondaryAction={{
|
||||||
|
label: 'Abbrechen',
|
||||||
|
onClick: onClose,
|
||||||
|
variant: 'secondary',
|
||||||
|
disabled: isSaving,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Wähle ein neues Profilbild aus oder entferne das aktuelle Bild.<br />
|
||||||
|
Unterstützte Formate: JPG, PNG, GIF.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Aktuell vs. Vorschau */}
|
||||||
|
<div className="mt-4 grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Aktuell
|
||||||
|
</span>
|
||||||
|
<PersonAvatar
|
||||||
|
name={avatarName}
|
||||||
|
avatarUrl={avatarUrl ?? undefined}
|
||||||
|
size="2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Vorschau
|
||||||
|
</span>
|
||||||
|
<PersonAvatar
|
||||||
|
name={avatarName}
|
||||||
|
avatarUrl={effectiveAvatarUrl}
|
||||||
|
size="2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File-Input + ggf. Lösch-Button */}
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="mt-2 block w-full text-sm text-gray-900
|
||||||
|
file:mr-4 file:rounded-md file:border-0
|
||||||
|
file:bg-indigo-50 file:px-3 file:py-1.5
|
||||||
|
file:text-sm file:font-semibold file:text-indigo-700
|
||||||
|
hover:file:bg-indigo-100
|
||||||
|
dark:text-gray-100
|
||||||
|
dark:file:bg-indigo-500/10 dark:file:text-indigo-200
|
||||||
|
dark:hover:file:bg-indigo-500/20"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Profilbild löschen nur, wenn wirklich eins vorhanden ist */}
|
||||||
|
{onAvatarDelete && hasDeletableAvatar && (
|
||||||
|
<Button
|
||||||
|
onClick={handleDeleteClick}
|
||||||
|
disabled={isSaving}
|
||||||
|
size='md'
|
||||||
|
variant='soft'
|
||||||
|
tone='rose'
|
||||||
|
className="w-full text-xs font-medium text-rose-600 hover:text-rose-700 dark:text-rose-400 dark:hover:text-rose-300"
|
||||||
|
>
|
||||||
|
Profilbild entfernen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { ChevronDownIcon } from '@heroicons/react/20/solid';
|
import { ChevronDownIcon } from '@heroicons/react/20/solid';
|
||||||
import { signOut } from 'next-auth/react';
|
import { signOut, useSession } from 'next-auth/react';
|
||||||
import PersonAvatar from '@/components/ui/UserAvatar';
|
import PersonAvatar from '@/components/ui/UserAvatar';
|
||||||
|
import ProfileAvatarModal from '@/components/ProfileAvatarModal';
|
||||||
|
|
||||||
export type UserMenuProps = {
|
export type UserMenuProps = {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -13,7 +14,7 @@ export type UserMenuProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const userNavigation = [
|
const userNavigation = [
|
||||||
{ name: 'Your profile', href: '#' },
|
{ name: 'Profilbild ändern', href: '#' },
|
||||||
{ name: 'Abmelden', href: '#' },
|
{ name: 'Abmelden', href: '#' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -23,6 +24,11 @@ export default function UserMenu({
|
|||||||
avatarUrl,
|
avatarUrl,
|
||||||
}: UserMenuProps) {
|
}: UserMenuProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [avatarModalOpen, setAvatarModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { update } = useSession();
|
||||||
|
|
||||||
|
const [currentAvatarUrl, setCurrentAvatarUrl] = useState<string | null | undefined>(avatarUrl ?? null);
|
||||||
|
|
||||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -66,15 +72,61 @@ export default function UserMenu({
|
|||||||
const handleItemClick = (itemName: string) => {
|
const handleItemClick = (itemName: string) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
|
if (itemName === 'Profilbild ändern') {
|
||||||
|
setAvatarModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (itemName === 'Abmelden') {
|
if (itemName === 'Abmelden') {
|
||||||
void signOut({ callbackUrl: '/login' });
|
void signOut({ callbackUrl: '/login' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// hier könntest du später noch Routing für "Your profile" o.ä. einbauen
|
const handleAvatarSelected = async (file: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', file);
|
||||||
|
|
||||||
|
const res = await fetch('/api/profile/avatar', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Avatar-Upload fehlgeschlagen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const newUrl = data.avatarUrl as string | null;
|
||||||
|
|
||||||
|
// 1) Lokal sofort aktualisieren
|
||||||
|
setCurrentAvatarUrl(newUrl);
|
||||||
|
|
||||||
|
// 2) NextAuth-Session aktualisieren → layout.tsx bekommt neue avatarUrl
|
||||||
|
await update({ avatarUrl: newUrl });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarDelete = async () => {
|
||||||
|
const res = await fetch('/api/profile/avatar', {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Avatar-Löschung fehlgeschlagen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Lokal zurück auf null
|
||||||
|
setCurrentAvatarUrl(null);
|
||||||
|
|
||||||
|
// 2) Session updaten → überall Fallback-Initialen
|
||||||
|
await update({ avatarUrl: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
@ -88,8 +140,7 @@ export default function UserMenu({
|
|||||||
<span className="absolute -inset-1.5" />
|
<span className="absolute -inset-1.5" />
|
||||||
<span className="sr-only">Open user menu</span>
|
<span className="sr-only">Open user menu</span>
|
||||||
|
|
||||||
{/* Avatar über gemeinsame Komponente */}
|
<PersonAvatar name={avatarName} avatarUrl={currentAvatarUrl} size="md" />
|
||||||
<PersonAvatar name={avatarName} avatarUrl={avatarUrl} size="md" />
|
|
||||||
|
|
||||||
<span className="hidden lg:flex lg:items-center">
|
<span className="hidden lg:flex lg:items-center">
|
||||||
<span
|
<span
|
||||||
@ -127,5 +178,16 @@ export default function UserMenu({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Profilbild-Modal */}
|
||||||
|
<ProfileAvatarModal
|
||||||
|
open={avatarModalOpen}
|
||||||
|
onClose={() => setAvatarModalOpen(false)}
|
||||||
|
avatarName={avatarName}
|
||||||
|
avatarUrl={currentAvatarUrl ?? undefined}
|
||||||
|
onAvatarSelected={handleAvatarSelected}
|
||||||
|
onAvatarDelete={handleAvatarDelete}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
239
components/ui/Alerts.tsx
Normal file
239
components/ui/Alerts.tsx
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
InformationCircleIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
} from '@heroicons/react/20/solid';
|
||||||
|
|
||||||
|
export type AlertTone = 'info' | 'success' | 'warning' | 'error';
|
||||||
|
|
||||||
|
export interface AlertProps {
|
||||||
|
/** Farbschema / Typ des Alerts */
|
||||||
|
tone?: AlertTone;
|
||||||
|
|
||||||
|
/** Überschrift (z.B. "Attention needed") */
|
||||||
|
title?: React.ReactNode;
|
||||||
|
|
||||||
|
/** Beschreibungstext (als string oder JSX) */
|
||||||
|
description?: React.ReactNode;
|
||||||
|
|
||||||
|
/** Bullet-List-Einträge wie im "With list"-Beispiel */
|
||||||
|
listItems?: React.ReactNode[];
|
||||||
|
|
||||||
|
/** Eigene Icon-Komponente (null = Icon komplett ausblenden) */
|
||||||
|
icon?: React.ReactNode | null;
|
||||||
|
|
||||||
|
/** Bereich für Buttons / Actions unter dem Text */
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
|
||||||
|
/** Inhalt rechts (z.B. ein "Details →"-Link) */
|
||||||
|
rightContent?: React.ReactNode;
|
||||||
|
|
||||||
|
/** Linker Accent-Border (statt Outlines im Dark Mode) */
|
||||||
|
accent?: boolean;
|
||||||
|
|
||||||
|
/** Wenn gesetzt, wird ein Dismiss-X angezeigt und dieser Handler aufgerufen */
|
||||||
|
onDismiss?: () => void;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toneConfig: Record<
|
||||||
|
AlertTone,
|
||||||
|
{
|
||||||
|
bg: string;
|
||||||
|
outline: string;
|
||||||
|
accentBorder: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
icon: string;
|
||||||
|
dismissBtn: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
info: {
|
||||||
|
bg: 'bg-blue-50 dark:bg-blue-500/10',
|
||||||
|
outline: 'dark:outline dark:outline-blue-500/20',
|
||||||
|
accentBorder: 'border-l-4 border-blue-400 dark:border-blue-500',
|
||||||
|
title: 'text-blue-800 dark:text-blue-200',
|
||||||
|
text: 'text-blue-700 dark:text-blue-300',
|
||||||
|
icon: 'text-blue-400 dark:text-blue-300',
|
||||||
|
dismissBtn:
|
||||||
|
'inline-flex rounded-md bg-blue-50 p-1.5 text-blue-500 hover:bg-blue-100 ' +
|
||||||
|
'focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 focus-visible:ring-offset-blue-50 focus-visible:outline-none ' +
|
||||||
|
'dark:bg-transparent dark:text-blue-400 dark:hover:bg-blue-500/10 ' +
|
||||||
|
'dark:focus-visible:ring-blue-500 dark:focus-visible:ring-offset-1 dark:focus-visible:ring-offset-blue-900',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
bg: 'bg-green-50 dark:bg-green-500/10',
|
||||||
|
outline: 'dark:outline dark:outline-green-500/20',
|
||||||
|
accentBorder: 'border-l-4 border-green-400 dark:border-green-500',
|
||||||
|
title: 'text-green-800 dark:text-green-200',
|
||||||
|
text: 'text-green-700 dark:text-green-200/85',
|
||||||
|
icon: 'text-green-400 dark:text-green-300',
|
||||||
|
dismissBtn:
|
||||||
|
'inline-flex rounded-md bg-green-50 p-1.5 text-green-500 hover:bg-green-100 ' +
|
||||||
|
'focus-visible:ring-2 focus-visible:ring-green-600 focus-visible:ring-offset-2 focus-visible:ring-offset-green-50 focus-visible:outline-none ' +
|
||||||
|
'dark:bg-transparent dark:text-green-400 dark:hover:bg-green-500/10 ' +
|
||||||
|
'dark:focus-visible:ring-green-500 dark:focus-visible:ring-offset-1 dark:focus-visible:ring-offset-green-900',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
bg: 'bg-yellow-50 dark:bg-yellow-500/10',
|
||||||
|
outline: 'dark:outline dark:outline-yellow-500/15',
|
||||||
|
accentBorder: 'border-l-4 border-yellow-400 dark:border-yellow-500',
|
||||||
|
title: 'text-yellow-800 dark:text-yellow-100',
|
||||||
|
text: 'text-yellow-700 dark:text-yellow-100/80',
|
||||||
|
icon: 'text-yellow-400 dark:text-yellow-300',
|
||||||
|
dismissBtn:
|
||||||
|
'inline-flex rounded-md bg-yellow-50 p-1.5 text-yellow-500 hover:bg-yellow-100 ' +
|
||||||
|
'focus-visible:ring-2 focus-visible:ring-yellow-600 focus-visible:ring-offset-2 focus-visible:ring-offset-yellow-50 focus-visible:outline-none ' +
|
||||||
|
'dark:bg-transparent dark:text-yellow-400 dark:hover:bg-yellow-500/10 ' +
|
||||||
|
'dark:focus-visible:ring-yellow-500 dark:focus-visible:ring-offset-1 dark:focus-visible:ring-offset-yellow-900',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
bg: 'bg-red-50 dark:bg-red-500/15',
|
||||||
|
outline: 'dark:outline dark:outline-red-500/25',
|
||||||
|
accentBorder: 'border-l-4 border-red-400 dark:border-red-500',
|
||||||
|
title: 'text-red-800 dark:text-red-200',
|
||||||
|
text: 'text-red-700 dark:text-red-200/80',
|
||||||
|
icon: 'text-red-400 dark:text-red-400',
|
||||||
|
dismissBtn:
|
||||||
|
'inline-flex rounded-md bg-red-50 p-1.5 text-red-500 hover:bg-red-100 ' +
|
||||||
|
'focus-visible:ring-2 focus-visible:ring-red-600 focus-visible:ring-offset-2 focus-visible:ring-offset-red-50 focus-visible:outline-none ' +
|
||||||
|
'dark:bg-transparent dark:text-red-400 dark:hover:bg-red-500/10 ' +
|
||||||
|
'dark:focus-visible:ring-red-500 dark:focus-visible:ring-offset-1 dark:focus-visible:ring-offset-red-900',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDefaultIcon(tone: AlertTone) {
|
||||||
|
switch (tone) {
|
||||||
|
case 'info':
|
||||||
|
return (
|
||||||
|
<InformationCircleIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-5"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'success':
|
||||||
|
return (
|
||||||
|
<CheckCircleIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-5"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'warning':
|
||||||
|
return (
|
||||||
|
<ExclamationTriangleIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-5"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'error':
|
||||||
|
return (
|
||||||
|
<XCircleIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-5"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Alerts({
|
||||||
|
tone = 'info',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
listItems,
|
||||||
|
icon,
|
||||||
|
actions,
|
||||||
|
rightContent,
|
||||||
|
accent = false,
|
||||||
|
onDismiss,
|
||||||
|
className,
|
||||||
|
}: AlertProps) {
|
||||||
|
const cfg = toneConfig[tone];
|
||||||
|
const hasList = !!listItems && listItems.length > 0;
|
||||||
|
const showIcon = icon !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
className={clsx(
|
||||||
|
'p-4',
|
||||||
|
cfg.bg,
|
||||||
|
accent ? cfg.accentBorder : 'rounded-md ' + cfg.outline,
|
||||||
|
accent && 'rounded-none',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
{showIcon && (
|
||||||
|
<div className="shrink-0">
|
||||||
|
<span className={cfg.icon}>
|
||||||
|
{icon ?? getDefaultIcon(tone)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'ml-3 flex-1',
|
||||||
|
rightContent || onDismiss
|
||||||
|
? 'md:flex md:justify-between md:items-start'
|
||||||
|
: undefined,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="text-sm">
|
||||||
|
{title && (
|
||||||
|
<h3 className={clsx('font-medium', cfg.title)}>{title}</h3>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<div className={clsx('mt-2', cfg.text)}>
|
||||||
|
{typeof description === 'string' ? (
|
||||||
|
<p>{description}</p>
|
||||||
|
) : (
|
||||||
|
description
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasList && (
|
||||||
|
<div className={clsx(title || description ? 'mt-2' : undefined, cfg.text)}>
|
||||||
|
<ul role="list" className="list-disc space-y-1 pl-5">
|
||||||
|
{listItems!.map((item, idx) => (
|
||||||
|
<li key={idx}>{item}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{actions && <div className="mt-4">{actions}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(rightContent || onDismiss) && (
|
||||||
|
<div className="mt-3 md:mt-0 md:ml-6 flex items-start gap-2">
|
||||||
|
{rightContent && <div className="text-sm">{rightContent}</div>}
|
||||||
|
|
||||||
|
{onDismiss && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onDismiss}
|
||||||
|
className={cfg.dismissBtn}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Dismiss</span>
|
||||||
|
<XMarkIcon aria-hidden="true" className="size-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Alerts;
|
||||||
@ -260,7 +260,7 @@ export default function AppCombobox<T>({
|
|||||||
|
|
||||||
<div className="relative mt-2">
|
<div className="relative mt-2">
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
className="block w-full rounded-md bg-white py-1.5 pr-12 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
|
className="block w-full rounded-md bg-white py-1.5 pr-12 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-gray-900 dark:text-gray-100 dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
39
components/ui/LoadingSpinner.tsx
Normal file
39
components/ui/LoadingSpinner.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// src/components/ui/LoadingSpinner.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type LoadingSpinnerProps = React.SVGProps<SVGSVGElement> & {
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses: Record<NonNullable<LoadingSpinnerProps['size']>, string> = {
|
||||||
|
xs: 'size-3',
|
||||||
|
sm: 'size-4',
|
||||||
|
md: 'size-5', // entspricht deinem Beispiel
|
||||||
|
lg: 'size-6',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LoadingSpinner({
|
||||||
|
size = 'md',
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: LoadingSpinnerProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className={clsx(
|
||||||
|
'animate-spin mr-3 -ml-1 text-gray-500 dark:text-gray-400',
|
||||||
|
sizeClasses[size],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
aria-hidden={props['aria-label'] ? undefined : true}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ChevronDownIcon } from '@heroicons/react/20/solid';
|
import { ChevronDownIcon } from '@heroicons/react/20/solid';
|
||||||
|
import LoadingSpinner from '@/components/ui/LoadingSpinner'; // 👈 Neu
|
||||||
|
|
||||||
function classNames(...classes: Array<string | boolean | null | undefined>) {
|
function classNames(...classes: Array<string | boolean | null | undefined>) {
|
||||||
return classes.filter(Boolean).join(' ');
|
return classes.filter(Boolean).join(' ');
|
||||||
@ -18,7 +19,7 @@ export type TableColumn<T> = {
|
|||||||
/** Kann die Spalte sortiert werden? */
|
/** Kann die Spalte sortiert werden? */
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
/** Kann die Spalte ausgeblendet werden? */
|
/** Kann die Spalte ausgeblendet werden? */
|
||||||
canHide?: boolean,
|
canHide?: boolean;
|
||||||
/** Optional eigene Klassen für die TH-Zelle */
|
/** Optional eigene Klassen für die TH-Zelle */
|
||||||
headerClassName?: string;
|
headerClassName?: string;
|
||||||
/** Optional eigene Klassen für die TD-Zelle */
|
/** Optional eigene Klassen für die TD-Zelle */
|
||||||
@ -44,6 +45,8 @@ export interface TableProps<T> {
|
|||||||
defaultSortKey?: keyof T;
|
defaultSortKey?: keyof T;
|
||||||
/** Optional: Standard-Sortierrichtung */
|
/** Optional: Standard-Sortierrichtung */
|
||||||
defaultSortDirection?: SortDirection;
|
defaultSortDirection?: SortDirection;
|
||||||
|
/** Optional: Wenn true, wird statt der Zeilen ein LoadingSpinner angezeigt */
|
||||||
|
isLoading?: boolean; // 👈 Neu
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortState<T> = {
|
type SortState<T> = {
|
||||||
@ -62,6 +65,7 @@ export default function Table<T>(props: TableProps<T>) {
|
|||||||
actionsHeader = '',
|
actionsHeader = '',
|
||||||
defaultSortKey,
|
defaultSortKey,
|
||||||
defaultSortDirection = 'asc',
|
defaultSortDirection = 'asc',
|
||||||
|
isLoading = false, // 👈 Neu
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [sort, setSort] = React.useState<SortState<T>>({
|
const [sort, setSort] = React.useState<SortState<T>>({
|
||||||
@ -87,12 +91,10 @@ export default function Table<T>(props: TableProps<T>) {
|
|||||||
if (va == null) return sort.direction === 'asc' ? -1 : 1;
|
if (va == null) return sort.direction === 'asc' ? -1 : 1;
|
||||||
if (vb == null) return sort.direction === 'asc' ? 1 : -1;
|
if (vb == null) return sort.direction === 'asc' ? 1 : -1;
|
||||||
|
|
||||||
// Reine Numbers
|
|
||||||
if (typeof va === 'number' && typeof vb === 'number') {
|
if (typeof va === 'number' && typeof vb === 'number') {
|
||||||
return sort.direction === 'asc' ? va - vb : vb - va;
|
return sort.direction === 'asc' ? va - vb : vb - va;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Numerische Strings wie "1", "123", "42"
|
|
||||||
if (typeof va === 'string' && typeof vb === 'string') {
|
if (typeof va === 'string' && typeof vb === 'string') {
|
||||||
const na = Number(va);
|
const na = Number(va);
|
||||||
const nb = Number(vb);
|
const nb = Number(vb);
|
||||||
@ -102,7 +104,6 @@ export default function Table<T>(props: TableProps<T>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date / ISO-String
|
|
||||||
const sa = va instanceof Date ? va.getTime() : String(va);
|
const sa = va instanceof Date ? va.getTime() : String(va);
|
||||||
const sb = vb instanceof Date ? vb.getTime() : String(vb);
|
const sb = vb instanceof Date ? vb.getTime() : String(vb);
|
||||||
|
|
||||||
@ -130,20 +131,20 @@ export default function Table<T>(props: TableProps<T>) {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!onSelectionChange) return;
|
if (!onSelectionChange) return;
|
||||||
const selectedRows = sortedData.filter((row) => selectedIds.includes(getRowId(row)));
|
const selectedRows = sortedData.filter((row) =>
|
||||||
|
selectedIds.includes(getRowId(row)),
|
||||||
|
);
|
||||||
onSelectionChange(selectedRows);
|
onSelectionChange(selectedRows);
|
||||||
}, [selectedIds, sortedData, getRowId, onSelectionChange]);
|
}, [selectedIds, sortedData, getRowId, onSelectionChange]);
|
||||||
|
|
||||||
function toggleSort(key: keyof T) {
|
function toggleSort(key: keyof T) {
|
||||||
setSort((prev) => {
|
setSort((prev) => {
|
||||||
if (prev.key === key) {
|
if (prev.key === key) {
|
||||||
// gleiche Spalte -> Richtung flippen
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
direction: prev.direction === 'asc' ? 'desc' : 'asc',
|
direction: prev.direction === 'asc' ? 'desc' : 'asc',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// neue Spalte -> asc
|
|
||||||
return { key, direction: 'asc' };
|
return { key, direction: 'asc' };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -151,7 +152,8 @@ export default function Table<T>(props: TableProps<T>) {
|
|||||||
function toggleAll() {
|
function toggleAll() {
|
||||||
if (!selectable) return;
|
if (!selectable) return;
|
||||||
const allIds = sortedData.map((row) => getRowId(row));
|
const allIds = sortedData.map((row) => getRowId(row));
|
||||||
const allSelected = allIds.length > 0 && allIds.every((id) => selectedIds.includes(id));
|
const allSelected =
|
||||||
|
allIds.length > 0 && allIds.every((id) => selectedIds.includes(id));
|
||||||
|
|
||||||
setSelectedIds(allSelected ? [] : allIds);
|
setSelectedIds(allSelected ? [] : allIds);
|
||||||
}
|
}
|
||||||
@ -163,13 +165,13 @@ export default function Table<T>(props: TableProps<T>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const colSpan =
|
||||||
|
columns.length + (selectable ? 1 : 0) + (renderActions ? 1 : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-visible rounded-lg border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900/40">
|
<div className="relative overflow-visible rounded-lg border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900/40">
|
||||||
{/* Wichtig: auf kleinen Screens overflow-x-visible, erst ab lg overflow-x-auto */}
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table
|
<table className="min-w-full table-fixed divide-y divide-gray-200 text-left text-sm dark:divide-white/10">
|
||||||
className="min-w-full table-fixed divide-y divide-gray-200 text-left text-sm dark:divide-white/10"
|
|
||||||
>
|
|
||||||
<thead className="bg-gray-50 dark:bg-gray-800/60">
|
<thead className="bg-gray-50 dark:bg-gray-800/60">
|
||||||
<tr>
|
<tr>
|
||||||
{selectable && (
|
{selectable && (
|
||||||
@ -239,7 +241,9 @@ export default function Table<T>(props: TableProps<T>) {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'size-4',
|
'size-4',
|
||||||
isSorted && sort.direction === 'desc' && 'rotate-180',
|
isSorted &&
|
||||||
|
sort.direction === 'desc' &&
|
||||||
|
'rotate-180',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@ -261,8 +265,34 @@ export default function Table<T>(props: TableProps<T>) {
|
|||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody className="divide-y divide-gray-200 bg-white dark:divide-white/10 dark:bg-gray-900/40">
|
<tbody className="divide-y divide-gray-200 bg-white dark:divide-white/10 dark:bg-gray-900/40">
|
||||||
{sortedData.map((row) => {
|
{isLoading ? (
|
||||||
|
// 🔹 Loading-State: Spinner-Zeile statt Daten
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={colSpan}
|
||||||
|
className="px-2 py-8 text-center text-sm text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
<LoadingSpinner />
|
||||||
|
<span>Wird geladen …</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : sortedData.length === 0 ? (
|
||||||
|
// 🔹 Empty-State
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={colSpan}
|
||||||
|
className="px-2 py-6 text-center text-sm text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
Keine Einträge vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
// 🔹 Normale Zeilen
|
||||||
|
sortedData.map((row) => {
|
||||||
const id = getRowId(row);
|
const id = getRowId(row);
|
||||||
const isSelected = selectedIds.includes(id);
|
const isSelected = selectedIds.includes(id);
|
||||||
|
|
||||||
@ -328,17 +358,7 @@ export default function Table<T>(props: TableProps<T>) {
|
|||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
|
||||||
{sortedData.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={columns.length + (selectable ? 1 : 0) + (renderActions ? 1 : 0)}
|
|
||||||
className="px-2 py-6 text-center text-sm text-gray-500 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
Keine Einträge vorhanden.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// components/ui/Tabs.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChevronDownIcon } from '@heroicons/react/16/solid';
|
import { ChevronDownIcon } from '@heroicons/react/16/solid';
|
||||||
@ -5,11 +6,21 @@ import clsx from 'clsx';
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Badge from '@/components/ui/Badge';
|
import Badge from '@/components/ui/Badge';
|
||||||
|
|
||||||
|
export type TabsVariant =
|
||||||
|
| 'underline' // Standard: Unterstrich, wie bisher (mit optionalem Count/Badge)
|
||||||
|
| 'underlineFull' // Full-width Tabs mit Unterstrich
|
||||||
|
| 'bar' // "Bar with underline" Variante
|
||||||
|
| 'pills' // Pills (neutral)
|
||||||
|
| 'pillsGray' // Pills auf grauem Hintergrund
|
||||||
|
| 'pillsBrand'; // Pills mit Brand-Farbe
|
||||||
|
|
||||||
export type TabItem = {
|
export type TabItem = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
/** optional: Anzahl (z.B. Personen in der Gruppe) */
|
/** optional: Anzahl / Badge, z.B. "52" */
|
||||||
count?: number;
|
count?: number | string;
|
||||||
|
/** optional: Icon (z.B. <UsersIcon className="size-5" />) */
|
||||||
|
icon?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TabsProps = {
|
type TabsProps = {
|
||||||
@ -18,6 +29,7 @@ type TabsProps = {
|
|||||||
onChange: (id: string) => void;
|
onChange: (id: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
variant?: TabsVariant;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Tabs({
|
export default function Tabs({
|
||||||
@ -26,15 +38,247 @@ export default function Tabs({
|
|||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
ariaLabel = 'Ansicht auswählen',
|
ariaLabel = 'Ansicht auswählen',
|
||||||
|
variant = 'underline',
|
||||||
}: TabsProps) {
|
}: TabsProps) {
|
||||||
const current = tabs.find((t) => t.id === value) ?? tabs[0];
|
if (!tabs || tabs.length === 0) return null;
|
||||||
|
|
||||||
|
const isValidValue = tabs.some((t) => t.id === value);
|
||||||
|
const currentId = isValidValue ? value : tabs[0].id;
|
||||||
|
const current = tabs.find((t) => t.id === currentId)!;
|
||||||
|
|
||||||
|
const renderDesktopTabs = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'underline':
|
||||||
|
return (
|
||||||
|
<div className="border-b border-gray-200 dark:border-white/10">
|
||||||
|
<nav aria-label={ariaLabel} className="-mb-px flex space-x-8">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isCurrent = tab.id === currentId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(tab.id)}
|
||||||
|
className={clsx(
|
||||||
|
isCurrent
|
||||||
|
? 'border-indigo-500 text-indigo-600 dark:border-indigo-400 dark:text-indigo-400'
|
||||||
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-white/20 dark:hover:text-gray-200',
|
||||||
|
'border-b-2 px-1 py-3 text-sm font-medium whitespace-nowrap flex items-center gap-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.icon && (
|
||||||
|
<span className="mr-1 -ml-0.5 flex items-center">
|
||||||
|
{tab.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
{tab.count != null && (
|
||||||
|
<Badge tone="gray" variant="flat" size="sm">
|
||||||
|
{tab.count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'underlineFull':
|
||||||
|
return (
|
||||||
|
<div className="border-b border-gray-200 dark:border-white/10">
|
||||||
|
<nav aria-label={ariaLabel} className="-mb-px flex">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isCurrent = tab.id === currentId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(tab.id)}
|
||||||
|
className={clsx(
|
||||||
|
isCurrent
|
||||||
|
? 'border-indigo-500 text-indigo-600 dark:border-indigo-400 dark:text-indigo-400'
|
||||||
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-white/20 dark:hover:text-gray-300',
|
||||||
|
'flex-1 border-b-2 px-1 py-3 text-center text-sm font-medium flex items-center justify-center gap-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.icon && (
|
||||||
|
<span className="mr-1 -ml-0.5 flex items-center">
|
||||||
|
{tab.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
{tab.count != null && (
|
||||||
|
<Badge tone="gray" variant="flat" size="sm">
|
||||||
|
{tab.count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'bar':
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className="isolate flex divide-x divide-gray-200 rounded-lg bg-white shadow-sm dark:divide-white/10 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10"
|
||||||
|
>
|
||||||
|
{tabs.map((tab, tabIdx) => {
|
||||||
|
const isCurrent = tab.id === currentId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(tab.id)}
|
||||||
|
className={clsx(
|
||||||
|
isCurrent
|
||||||
|
? 'text-gray-900 dark:text-white'
|
||||||
|
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-white',
|
||||||
|
tabIdx === 0 ? 'rounded-l-lg' : '',
|
||||||
|
tabIdx === tabs.length - 1 ? 'rounded-r-lg' : '',
|
||||||
|
'group relative min-w-0 flex-1 overflow-hidden px-4 py-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10 dark:hover:bg-white/5 flex items-center justify-center gap-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.icon && (
|
||||||
|
<span className="mr-1 -ml-0.5 flex items-center">
|
||||||
|
{tab.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
{tab.count != null && (
|
||||||
|
<Badge tone="gray" variant="flat" size="sm">
|
||||||
|
{tab.count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={clsx(
|
||||||
|
isCurrent ? 'bg-indigo-500 dark:bg-indigo-400' : 'bg-transparent',
|
||||||
|
'absolute inset-x-0 bottom-0 h-0.5',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'pills':
|
||||||
|
return (
|
||||||
|
<nav aria-label={ariaLabel} className="flex space-x-4">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isCurrent = tab.id === currentId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(tab.id)}
|
||||||
|
className={clsx(
|
||||||
|
isCurrent
|
||||||
|
? 'bg-gray-100 text-gray-700 dark:bg-white/10 dark:text-gray-200'
|
||||||
|
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200',
|
||||||
|
'rounded-md px-3 py-2 text-sm font-medium flex items-center gap-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.icon && (
|
||||||
|
<span className="mr-1 -ml-0.5 flex items-center">
|
||||||
|
{tab.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
{tab.count != null && (
|
||||||
|
<Badge tone="gray" variant="flat" size="sm">
|
||||||
|
{tab.count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'pillsGray':
|
||||||
|
return (
|
||||||
|
<nav aria-label={ariaLabel} className="flex space-x-4">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isCurrent = tab.id === currentId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(tab.id)}
|
||||||
|
className={clsx(
|
||||||
|
isCurrent
|
||||||
|
? 'bg-gray-200 text-gray-800 dark:bg-white/10 dark:text-white'
|
||||||
|
: 'text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-white',
|
||||||
|
'rounded-md px-3 py-2 text-sm font-medium flex items-center gap-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.icon && (
|
||||||
|
<span className="mr-1 -ml-0.5 flex items-center">
|
||||||
|
{tab.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
{tab.count != null && (
|
||||||
|
<Badge tone="gray" variant="flat" size="sm">
|
||||||
|
{tab.count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'pillsBrand':
|
||||||
|
return (
|
||||||
|
<nav aria-label={ariaLabel} className="flex space-x-4">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isCurrent = tab.id === currentId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(tab.id)}
|
||||||
|
className={clsx(
|
||||||
|
isCurrent
|
||||||
|
? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300'
|
||||||
|
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200',
|
||||||
|
'rounded-md px-3 py-2 text-sm font-medium flex items-center gap-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.icon && (
|
||||||
|
<span className="mr-1 -ml-0.5 flex items-center">
|
||||||
|
{tab.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
{tab.count != null && (
|
||||||
|
<Badge tone="gray" variant="flat" size="sm">
|
||||||
|
{tab.count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{/* Mobile: Select + Chevron */}
|
{/* Mobile: Select + Chevron (für alle Varianten gleich) */}
|
||||||
<div className="grid grid-cols-1 sm:hidden">
|
<div className="grid grid-cols-1 sm:hidden">
|
||||||
<select
|
<select
|
||||||
value={current?.id}
|
value={currentId}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
className="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-2 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 dark:bg-white/5 dark:text-gray-100 dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500"
|
className="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-2 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 dark:bg-white/5 dark:text-gray-100 dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500"
|
||||||
@ -53,34 +297,8 @@ export default function Tabs({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop: Underline-Tabs */}
|
{/* Desktop: abhängig von variant */}
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">{renderDesktopTabs()}</div>
|
||||||
<div className="border-b border-gray-200 dark:border-white/10">
|
|
||||||
<nav aria-label={ariaLabel} className="-mb-px flex space-x-8">
|
|
||||||
{tabs.map((tab) => {
|
|
||||||
const isCurrent = tab.id === current?.id;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={tab.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onChange(tab.id)}
|
|
||||||
className={clsx(
|
|
||||||
isCurrent
|
|
||||||
? 'border-indigo-500 text-indigo-600 dark:border-indigo-400 dark:text-indigo-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-white/20 dark:hover:text-gray-200',
|
|
||||||
'border-b-2 px-1 py-3 text-sm font-medium whitespace-nowrap flex items-center gap-2',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{tab.label}</span>
|
|
||||||
{typeof tab.count === 'number' && (
|
|
||||||
<Badge tone="gray" variant="flat" size="sm">{tab.count}</Badge>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,12 +23,15 @@ function getAvatarColor(seed: string) {
|
|||||||
return AVATAR_COLORS[index];
|
return AVATAR_COLORS[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
type Size = 'sm' | 'md' | 'lg';
|
type Size = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
|
||||||
|
|
||||||
const sizeClasses: Record<Size, string> = {
|
const sizeClasses: Record<Size, string> = {
|
||||||
sm: 'h-6 w-6 text-[10px]',
|
sm: 'h-6 w-6 text-[10px]',
|
||||||
md: 'h-8 w-8 text-xs',
|
md: 'h-8 w-8 text-xs',
|
||||||
lg: 'h-10 w-10 text-sm',
|
lg: 'h-10 w-10 text-sm',
|
||||||
|
xl: 'h-12 w-12 text-md',
|
||||||
|
'2xl': 'h-14 w-14 text-lg',
|
||||||
|
'3xl': 'h-16 w-16 text-xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserAvatarProps = {
|
export type UserAvatarProps = {
|
||||||
@ -49,16 +52,28 @@ export default function UserAvatar({
|
|||||||
const initial = displayName.charAt(0)?.toUpperCase() || '?';
|
const initial = displayName.charAt(0)?.toUpperCase() || '?';
|
||||||
const colorClass = getAvatarColor(displayName || initial || 'x');
|
const colorClass = getAvatarColor(displayName || initial || 'x');
|
||||||
|
|
||||||
if (avatarUrl) {
|
// Wenn Bild-URL gesetzt, aber das Laden fehlschlägt → Fallback auf Initialen
|
||||||
|
const [hasImageError, setHasImageError] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Wenn sich avatarUrl ändert, Fehlerzustand zurücksetzen
|
||||||
|
setHasImageError(false);
|
||||||
|
}, [avatarUrl]);
|
||||||
|
|
||||||
|
const showImage = !!avatarUrl && !hasImageError;
|
||||||
|
|
||||||
|
if (showImage) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={avatarUrl}
|
src={avatarUrl!}
|
||||||
alt={displayName || 'Avatar'}
|
alt={displayName || 'Avatar'}
|
||||||
|
onError={() => setHasImageError(true)}
|
||||||
className={clsx('rounded-full object-cover', sizeClasses[size])}
|
className={clsx('rounded-full object-cover', sizeClasses[size])}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: Initialen mit pseudo-zufälliger Hintergrundfarbe
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export * from "./enums.ts"
|
|||||||
* const users = await prisma.user.findMany()
|
* const users = await prisma.user.findMany()
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
*/
|
*/
|
||||||
export const PrismaClient = $Class.getPrismaClientClass()
|
export const PrismaClient = $Class.getPrismaClientClass()
|
||||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -80,12 +80,12 @@ export type PrismaVersion = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prisma Client JS version: 7.0.0
|
* Prisma Client JS version: 7.1.0
|
||||||
* Query Engine version: 0c19ccc313cf9911a90d99d2ac2eb0280c76c513
|
* Query Engine version: ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba
|
||||||
*/
|
*/
|
||||||
export const prismaVersion: PrismaVersion = {
|
export const prismaVersion: PrismaVersion = {
|
||||||
client: "7.0.0",
|
client: "7.1.0",
|
||||||
engine: "0c19ccc313cf9911a90d99d2ac2eb0280c76c513"
|
engine: "ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1126,7 +1126,8 @@ export const UserScalarFieldEnum = {
|
|||||||
passwordHash: 'passwordHash',
|
passwordHash: 'passwordHash',
|
||||||
groupId: 'groupId',
|
groupId: 'groupId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt',
|
||||||
|
avatarUrl: 'avatarUrl'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||||
@ -1394,7 +1395,7 @@ export type PrismaClientOptions = ({
|
|||||||
* { emit: 'stdout', level: 'error' }
|
* { emit: 'stdout', level: 'error' }
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/logging#the-log-option).
|
* Read more in our [docs](https://pris.ly/d/logging).
|
||||||
*/
|
*/
|
||||||
log?: (LogLevel | LogDefinition)[]
|
log?: (LogLevel | LogDefinition)[]
|
||||||
/**
|
/**
|
||||||
@ -1422,6 +1423,22 @@ export type PrismaClientOptions = ({
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
omit?: GlobalOmitConfig
|
omit?: GlobalOmitConfig
|
||||||
|
/**
|
||||||
|
* SQL commenter plugins that add metadata to SQL queries as comments.
|
||||||
|
* Comments follow the sqlcommenter format: https://google.github.io/sqlcommenter/
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient({
|
||||||
|
* adapter,
|
||||||
|
* comments: [
|
||||||
|
* traceContext(),
|
||||||
|
* queryInsights(),
|
||||||
|
* ],
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
comments?: runtime.SqlCommenterPlugin[]
|
||||||
}
|
}
|
||||||
export type GlobalOmitConfig = {
|
export type GlobalOmitConfig = {
|
||||||
user?: Prisma.UserOmit
|
user?: Prisma.UserOmit
|
||||||
|
|||||||
@ -87,7 +87,8 @@ export const UserScalarFieldEnum = {
|
|||||||
passwordHash: 'passwordHash',
|
passwordHash: 'passwordHash',
|
||||||
groupId: 'groupId',
|
groupId: 'groupId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt',
|
||||||
|
avatarUrl: 'avatarUrl'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export type UserMinAggregateOutputType = {
|
|||||||
groupId: string | null
|
groupId: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
|
avatarUrl: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserMaxAggregateOutputType = {
|
export type UserMaxAggregateOutputType = {
|
||||||
@ -46,6 +47,7 @@ export type UserMaxAggregateOutputType = {
|
|||||||
groupId: string | null
|
groupId: string | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
|
avatarUrl: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCountAggregateOutputType = {
|
export type UserCountAggregateOutputType = {
|
||||||
@ -58,6 +60,7 @@ export type UserCountAggregateOutputType = {
|
|||||||
groupId: number
|
groupId: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
|
avatarUrl: number
|
||||||
_all: number
|
_all: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +75,7 @@ export type UserMinAggregateInputType = {
|
|||||||
groupId?: true
|
groupId?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
|
avatarUrl?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserMaxAggregateInputType = {
|
export type UserMaxAggregateInputType = {
|
||||||
@ -84,6 +88,7 @@ export type UserMaxAggregateInputType = {
|
|||||||
groupId?: true
|
groupId?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
|
avatarUrl?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCountAggregateInputType = {
|
export type UserCountAggregateInputType = {
|
||||||
@ -96,6 +101,7 @@ export type UserCountAggregateInputType = {
|
|||||||
groupId?: true
|
groupId?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
|
avatarUrl?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,6 +187,7 @@ export type UserGroupByOutputType = {
|
|||||||
groupId: string | null
|
groupId: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
avatarUrl: string | null
|
||||||
_count: UserCountAggregateOutputType | null
|
_count: UserCountAggregateOutputType | null
|
||||||
_min: UserMinAggregateOutputType | null
|
_min: UserMinAggregateOutputType | null
|
||||||
_max: UserMaxAggregateOutputType | null
|
_max: UserMaxAggregateOutputType | null
|
||||||
@ -214,6 +221,7 @@ export type UserWhereInput = {
|
|||||||
groupId?: Prisma.StringNullableFilter<"User"> | string | null
|
groupId?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
|
avatarUrl?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
devicesCreated?: Prisma.DeviceListRelationFilter
|
devicesCreated?: Prisma.DeviceListRelationFilter
|
||||||
devicesUpdated?: Prisma.DeviceListRelationFilter
|
devicesUpdated?: Prisma.DeviceListRelationFilter
|
||||||
historyEntries?: Prisma.DeviceHistoryListRelationFilter
|
historyEntries?: Prisma.DeviceHistoryListRelationFilter
|
||||||
@ -231,6 +239,7 @@ export type UserOrderByWithRelationInput = {
|
|||||||
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
|
avatarUrl?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
devicesCreated?: Prisma.DeviceOrderByRelationAggregateInput
|
devicesCreated?: Prisma.DeviceOrderByRelationAggregateInput
|
||||||
devicesUpdated?: Prisma.DeviceOrderByRelationAggregateInput
|
devicesUpdated?: Prisma.DeviceOrderByRelationAggregateInput
|
||||||
historyEntries?: Prisma.DeviceHistoryOrderByRelationAggregateInput
|
historyEntries?: Prisma.DeviceHistoryOrderByRelationAggregateInput
|
||||||
@ -251,6 +260,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
groupId?: Prisma.StringNullableFilter<"User"> | string | null
|
groupId?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
|
avatarUrl?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
devicesCreated?: Prisma.DeviceListRelationFilter
|
devicesCreated?: Prisma.DeviceListRelationFilter
|
||||||
devicesUpdated?: Prisma.DeviceListRelationFilter
|
devicesUpdated?: Prisma.DeviceListRelationFilter
|
||||||
historyEntries?: Prisma.DeviceHistoryListRelationFilter
|
historyEntries?: Prisma.DeviceHistoryListRelationFilter
|
||||||
@ -268,6 +278,7 @@ export type UserOrderByWithAggregationInput = {
|
|||||||
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
groupId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
|
avatarUrl?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
_count?: Prisma.UserCountOrderByAggregateInput
|
_count?: Prisma.UserCountOrderByAggregateInput
|
||||||
_max?: Prisma.UserMaxOrderByAggregateInput
|
_max?: Prisma.UserMaxOrderByAggregateInput
|
||||||
_min?: Prisma.UserMinOrderByAggregateInput
|
_min?: Prisma.UserMinOrderByAggregateInput
|
||||||
@ -286,6 +297,7 @@ export type UserScalarWhereWithAggregatesInput = {
|
|||||||
groupId?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
groupId?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
||||||
|
avatarUrl?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateInput = {
|
export type UserCreateInput = {
|
||||||
@ -297,6 +309,7 @@ export type UserCreateInput = {
|
|||||||
passwordHash?: string | null
|
passwordHash?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
||||||
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
||||||
@ -314,6 +327,7 @@ export type UserUncheckedCreateInput = {
|
|||||||
groupId?: string | null
|
groupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
||||||
@ -329,6 +343,7 @@ export type UserUpdateInput = {
|
|||||||
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
||||||
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
||||||
@ -346,6 +361,7 @@ export type UserUncheckedUpdateInput = {
|
|||||||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
||||||
@ -362,6 +378,7 @@ export type UserCreateManyInput = {
|
|||||||
groupId?: string | null
|
groupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUpdateManyMutationInput = {
|
export type UserUpdateManyMutationInput = {
|
||||||
@ -373,6 +390,7 @@ export type UserUpdateManyMutationInput = {
|
|||||||
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateManyInput = {
|
export type UserUncheckedUpdateManyInput = {
|
||||||
@ -385,6 +403,7 @@ export type UserUncheckedUpdateManyInput = {
|
|||||||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCountOrderByAggregateInput = {
|
export type UserCountOrderByAggregateInput = {
|
||||||
@ -397,6 +416,7 @@ export type UserCountOrderByAggregateInput = {
|
|||||||
groupId?: Prisma.SortOrder
|
groupId?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
|
avatarUrl?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserMaxOrderByAggregateInput = {
|
export type UserMaxOrderByAggregateInput = {
|
||||||
@ -409,6 +429,7 @@ export type UserMaxOrderByAggregateInput = {
|
|||||||
groupId?: Prisma.SortOrder
|
groupId?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
|
avatarUrl?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserMinOrderByAggregateInput = {
|
export type UserMinOrderByAggregateInput = {
|
||||||
@ -421,6 +442,7 @@ export type UserMinOrderByAggregateInput = {
|
|||||||
groupId?: Prisma.SortOrder
|
groupId?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
|
avatarUrl?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserScalarRelationFilter = {
|
export type UserScalarRelationFilter = {
|
||||||
@ -568,6 +590,7 @@ export type UserCreateWithoutRolesInput = {
|
|||||||
passwordHash?: string | null
|
passwordHash?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
||||||
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
||||||
@ -584,6 +607,7 @@ export type UserUncheckedCreateWithoutRolesInput = {
|
|||||||
groupId?: string | null
|
groupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
||||||
@ -614,6 +638,7 @@ export type UserUpdateWithoutRolesInput = {
|
|||||||
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
||||||
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
||||||
@ -630,6 +655,7 @@ export type UserUncheckedUpdateWithoutRolesInput = {
|
|||||||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
||||||
@ -644,6 +670,7 @@ export type UserCreateWithoutGroupInput = {
|
|||||||
passwordHash?: string | null
|
passwordHash?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
||||||
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
||||||
@ -659,6 +686,7 @@ export type UserUncheckedCreateWithoutGroupInput = {
|
|||||||
passwordHash?: string | null
|
passwordHash?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
||||||
@ -704,6 +732,7 @@ export type UserScalarWhereInput = {
|
|||||||
groupId?: Prisma.StringNullableFilter<"User"> | string | null
|
groupId?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
|
avatarUrl?: Prisma.StringNullableFilter<"User"> | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateWithoutDevicesCreatedInput = {
|
export type UserCreateWithoutDevicesCreatedInput = {
|
||||||
@ -715,6 +744,7 @@ export type UserCreateWithoutDevicesCreatedInput = {
|
|||||||
passwordHash?: string | null
|
passwordHash?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
||||||
group?: Prisma.UserGroupCreateNestedOneWithoutUsersInput
|
group?: Prisma.UserGroupCreateNestedOneWithoutUsersInput
|
||||||
@ -731,6 +761,7 @@ export type UserUncheckedCreateWithoutDevicesCreatedInput = {
|
|||||||
groupId?: string | null
|
groupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
||||||
roles?: Prisma.UserRoleUncheckedCreateNestedManyWithoutUserInput
|
roles?: Prisma.UserRoleUncheckedCreateNestedManyWithoutUserInput
|
||||||
@ -750,6 +781,7 @@ export type UserCreateWithoutDevicesUpdatedInput = {
|
|||||||
passwordHash?: string | null
|
passwordHash?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryCreateNestedManyWithoutChangedByInput
|
||||||
group?: Prisma.UserGroupCreateNestedOneWithoutUsersInput
|
group?: Prisma.UserGroupCreateNestedOneWithoutUsersInput
|
||||||
@ -766,6 +798,7 @@ export type UserUncheckedCreateWithoutDevicesUpdatedInput = {
|
|||||||
groupId?: string | null
|
groupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutChangedByInput
|
||||||
roles?: Prisma.UserRoleUncheckedCreateNestedManyWithoutUserInput
|
roles?: Prisma.UserRoleUncheckedCreateNestedManyWithoutUserInput
|
||||||
@ -796,6 +829,7 @@ export type UserUpdateWithoutDevicesCreatedInput = {
|
|||||||
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
||||||
group?: Prisma.UserGroupUpdateOneWithoutUsersNestedInput
|
group?: Prisma.UserGroupUpdateOneWithoutUsersNestedInput
|
||||||
@ -812,6 +846,7 @@ export type UserUncheckedUpdateWithoutDevicesCreatedInput = {
|
|||||||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
||||||
roles?: Prisma.UserRoleUncheckedUpdateManyWithoutUserNestedInput
|
roles?: Prisma.UserRoleUncheckedUpdateManyWithoutUserNestedInput
|
||||||
@ -837,6 +872,7 @@ export type UserUpdateWithoutDevicesUpdatedInput = {
|
|||||||
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
||||||
group?: Prisma.UserGroupUpdateOneWithoutUsersNestedInput
|
group?: Prisma.UserGroupUpdateOneWithoutUsersNestedInput
|
||||||
@ -853,6 +889,7 @@ export type UserUncheckedUpdateWithoutDevicesUpdatedInput = {
|
|||||||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
||||||
roles?: Prisma.UserRoleUncheckedUpdateManyWithoutUserNestedInput
|
roles?: Prisma.UserRoleUncheckedUpdateManyWithoutUserNestedInput
|
||||||
@ -867,6 +904,7 @@ export type UserCreateWithoutHistoryEntriesInput = {
|
|||||||
passwordHash?: string | null
|
passwordHash?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceCreateNestedManyWithoutCreatedByInput
|
||||||
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceCreateNestedManyWithoutUpdatedByInput
|
||||||
group?: Prisma.UserGroupCreateNestedOneWithoutUsersInput
|
group?: Prisma.UserGroupCreateNestedOneWithoutUsersInput
|
||||||
@ -883,6 +921,7 @@ export type UserUncheckedCreateWithoutHistoryEntriesInput = {
|
|||||||
groupId?: string | null
|
groupId?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
devicesCreated?: Prisma.DeviceUncheckedCreateNestedManyWithoutCreatedByInput
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
devicesUpdated?: Prisma.DeviceUncheckedCreateNestedManyWithoutUpdatedByInput
|
||||||
roles?: Prisma.UserRoleUncheckedCreateNestedManyWithoutUserInput
|
roles?: Prisma.UserRoleUncheckedCreateNestedManyWithoutUserInput
|
||||||
@ -913,6 +952,7 @@ export type UserUpdateWithoutHistoryEntriesInput = {
|
|||||||
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
||||||
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
||||||
group?: Prisma.UserGroupUpdateOneWithoutUsersNestedInput
|
group?: Prisma.UserGroupUpdateOneWithoutUsersNestedInput
|
||||||
@ -929,6 +969,7 @@ export type UserUncheckedUpdateWithoutHistoryEntriesInput = {
|
|||||||
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
||||||
roles?: Prisma.UserRoleUncheckedUpdateManyWithoutUserNestedInput
|
roles?: Prisma.UserRoleUncheckedUpdateManyWithoutUserNestedInput
|
||||||
@ -943,6 +984,7 @@ export type UserCreateManyGroupInput = {
|
|||||||
passwordHash?: string | null
|
passwordHash?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
avatarUrl?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUpdateWithoutGroupInput = {
|
export type UserUpdateWithoutGroupInput = {
|
||||||
@ -954,6 +996,7 @@ export type UserUpdateWithoutGroupInput = {
|
|||||||
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUpdateManyWithoutCreatedByNestedInput
|
||||||
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUpdateManyWithoutUpdatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUpdateManyWithoutChangedByNestedInput
|
||||||
@ -969,6 +1012,7 @@ export type UserUncheckedUpdateWithoutGroupInput = {
|
|||||||
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
devicesCreated?: Prisma.DeviceUncheckedUpdateManyWithoutCreatedByNestedInput
|
||||||
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
devicesUpdated?: Prisma.DeviceUncheckedUpdateManyWithoutUpdatedByNestedInput
|
||||||
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
historyEntries?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutChangedByNestedInput
|
||||||
@ -984,6 +1028,7 @@ export type UserUncheckedUpdateManyWithoutGroupInput = {
|
|||||||
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
avatarUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1054,6 +1099,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
groupId?: boolean
|
groupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
|
avatarUrl?: boolean
|
||||||
devicesCreated?: boolean | Prisma.User$devicesCreatedArgs<ExtArgs>
|
devicesCreated?: boolean | Prisma.User$devicesCreatedArgs<ExtArgs>
|
||||||
devicesUpdated?: boolean | Prisma.User$devicesUpdatedArgs<ExtArgs>
|
devicesUpdated?: boolean | Prisma.User$devicesUpdatedArgs<ExtArgs>
|
||||||
historyEntries?: boolean | Prisma.User$historyEntriesArgs<ExtArgs>
|
historyEntries?: boolean | Prisma.User$historyEntriesArgs<ExtArgs>
|
||||||
@ -1072,6 +1118,7 @@ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
groupId?: boolean
|
groupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
|
avatarUrl?: boolean
|
||||||
group?: boolean | Prisma.User$groupArgs<ExtArgs>
|
group?: boolean | Prisma.User$groupArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
@ -1085,6 +1132,7 @@ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
groupId?: boolean
|
groupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
|
avatarUrl?: boolean
|
||||||
group?: boolean | Prisma.User$groupArgs<ExtArgs>
|
group?: boolean | Prisma.User$groupArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
@ -1098,9 +1146,10 @@ export type UserSelectScalar = {
|
|||||||
groupId?: boolean
|
groupId?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
|
avatarUrl?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"nwkennung" | "email" | "arbeitsname" | "firstName" | "lastName" | "passwordHash" | "groupId" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
|
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"nwkennung" | "email" | "arbeitsname" | "firstName" | "lastName" | "passwordHash" | "groupId" | "createdAt" | "updatedAt" | "avatarUrl", ExtArgs["result"]["user"]>
|
||||||
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
devicesCreated?: boolean | Prisma.User$devicesCreatedArgs<ExtArgs>
|
devicesCreated?: boolean | Prisma.User$devicesCreatedArgs<ExtArgs>
|
||||||
devicesUpdated?: boolean | Prisma.User$devicesUpdatedArgs<ExtArgs>
|
devicesUpdated?: boolean | Prisma.User$devicesUpdatedArgs<ExtArgs>
|
||||||
@ -1135,6 +1184,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
groupId: string | null
|
groupId: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
avatarUrl: string | null
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
composites: {}
|
composites: {}
|
||||||
}
|
}
|
||||||
@ -1572,6 +1622,7 @@ export interface UserFieldRefs {
|
|||||||
readonly groupId: Prisma.FieldRef<"User", 'String'>
|
readonly groupId: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
|
readonly avatarUrl: Prisma.FieldRef<"User", 'String'>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,10 @@ export const authOptions: NextAuthOptions = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
group: true, // <-- wichtig, damit user.group da ist
|
||||||
|
// roles: { include: { role: true } }, // falls du das auch brauchst
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user || !user.passwordHash) return null;
|
if (!user || !user.passwordHash) return null;
|
||||||
@ -55,11 +59,15 @@ export const authOptions: NextAuthOptions = {
|
|||||||
? `${user.firstName} ${user.lastName}`
|
? `${user.firstName} ${user.lastName}`
|
||||||
: user.email ?? user.nwkennung ?? 'Unbekannt');
|
: user.email ?? user.nwkennung ?? 'Unbekannt');
|
||||||
|
|
||||||
|
const canEditDevices = !!user.group?.canEditDevices;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.nwkennung,
|
id: user.nwkennung,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
nwkennung: user.nwkennung,
|
nwkennung: user.nwkennung,
|
||||||
|
avatarUrl: user.avatarUrl ?? null,
|
||||||
|
groupCanEditDevices: canEditDevices, // <-- hier mitgeben
|
||||||
} as any;
|
} as any;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -69,23 +77,39 @@ export const authOptions: NextAuthOptions = {
|
|||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
strategy: 'jwt',
|
strategy: 'jwt',
|
||||||
// Login wird standardmäßig gemerkt (hier explizit: 30 Tage)
|
|
||||||
maxAge: 60 * 60 * 24 * 30,
|
maxAge: 60 * 60 * 24 * 30,
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user, trigger, session }) {
|
||||||
|
// Wenn wir clientseitig `update({ avatarUrl })` aufrufen,
|
||||||
|
// kommt das hier als `trigger === 'update'` an.
|
||||||
|
if (trigger === 'update' && session) {
|
||||||
|
if ('avatarUrl' in session) {
|
||||||
|
(token as any).avatarUrl = (session as any).avatarUrl ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login-Fall
|
||||||
if (user) {
|
if (user) {
|
||||||
token.id = (user as any).id;
|
token.id = (user as any).id;
|
||||||
token.nwkennung = (user as any).nwkennung;
|
token.nwkennung = (user as any).nwkennung;
|
||||||
|
(token as any).avatarUrl = (user as any).avatarUrl ?? null;
|
||||||
|
(token as any).groupCanEditDevices =
|
||||||
|
(user as any).groupCanEditDevices ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
|
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (session.user && token.id) {
|
if (session.user && token.id) {
|
||||||
(session.user as any).id = token.id;
|
(session.user as any).id = token.id;
|
||||||
(session.user as any).nwkennung = token.nwkennung;
|
(session.user as any).nwkennung = (token as any).nwkennung;
|
||||||
|
(session.user as any).avatarUrl = (token as any).avatarUrl ?? null;
|
||||||
|
(session.user as any).groupCanEditDevices =
|
||||||
|
(token as any).groupCanEditDevices ?? false;
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
132
package-lock.json
generated
132
package-lock.json
generated
@ -12,7 +12,7 @@
|
|||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@prisma/adapter-better-sqlite3": "^7.0.0",
|
"@prisma/adapter-better-sqlite3": "^7.0.0",
|
||||||
"@prisma/adapter-pg": "^7.0.0",
|
"@prisma/adapter-pg": "^7.0.0",
|
||||||
"@prisma/client": "^7.0.1",
|
"@prisma/client": "^7.1.0",
|
||||||
"@zxing/browser": "^0.1.5",
|
"@zxing/browser": "^0.1.5",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"next": "16.0.3",
|
"next": "16.0.3",
|
||||||
@ -36,7 +36,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.0.3",
|
"eslint-config-next": "16.0.3",
|
||||||
"prisma": "^7.0.1",
|
"prisma": "^7.1.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
@ -1095,9 +1095,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@hono/node-server": {
|
"node_modules/@hono/node-server": {
|
||||||
"version": "1.14.2",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz",
|
||||||
"integrity": "sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A==",
|
"integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -1923,12 +1923,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.1.0.tgz",
|
||||||
"integrity": "sha512-O74T6xcfaGAq5gXwCAvfTLvI6fmC3and2g5yLRMkNjri1K8mSpEgclDNuUWs9xj5AwNEMQ88NeD3asI+sovm1g==",
|
"integrity": "sha512-qf7GPYHmS/xybNiSOpzv9wBo+UwqfL2PeyX+08v+KVHDI0AlSCQIh5bBySkH3alu06NX9wy98JEnckhMHoMFfA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client-runtime-utils": "7.0.1"
|
"@prisma/client-runtime-utils": "7.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19 || ^22.12 || >=24.0"
|
"node": "^20.19 || ^22.12 || >=24.0"
|
||||||
@ -1947,15 +1947,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client-runtime-utils": {
|
"node_modules/@prisma/client-runtime-utils": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.1.0.tgz",
|
||||||
"integrity": "sha512-R26BVX9D/iw4toUmZKZf3jniM/9pMGHHdZN5LVP2L7HNiCQKNQQx/9LuMtjepbgRqSqQO3oHN0yzojHLnKTGEw==",
|
"integrity": "sha512-39xmeBrNTN40FzF34aJMjfX1PowVCqoT3UKUWBBSP3aXV05NRqGBC3x2wCDs96ti6ZgdiVzqnRDHtbzU8X+lPQ==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/config": {
|
"node_modules/@prisma/config": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.1.0.tgz",
|
||||||
"integrity": "sha512-MacIjXdo+hNKxPvtMzDXykIIc8HCRWoyjQ2nguJTFqLDzJBD5L6QRaANGTLOqbGtJ3sFvLRmfXhrFg3pWoK1BA==",
|
"integrity": "sha512-Uz+I43Wn1RYNHtuYtOhOnUcNMWp2Pd3GUDDKs37xlHptCGpzEG3MRR9L+8Y2ISMsMI24z/Ni+ww6OB/OO8M0sQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1972,28 +1972,28 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/dev": {
|
"node_modules/@prisma/dev": {
|
||||||
"version": "0.13.0",
|
"version": "0.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.15.0.tgz",
|
||||||
"integrity": "sha512-QMmF6zFeUF78yv1HYbHvod83AQnl7u6NtKyDhTRZOJup3h1icWs8R7RUVxBJZvM2tBXNAMpLQYYM/8kPlOPegA==",
|
"integrity": "sha512-KhWaipnFlS/fWEs6I6Oqjcy2S08vKGmxJ5LexqUl/3Ve0EgLUsZwdKF0MvqPM5F5ttw8GtfZarjM5y7VLwv9Ow==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electric-sql/pglite": "0.3.2",
|
"@electric-sql/pglite": "0.3.2",
|
||||||
"@electric-sql/pglite-socket": "0.0.6",
|
"@electric-sql/pglite-socket": "0.0.6",
|
||||||
"@electric-sql/pglite-tools": "0.2.7",
|
"@electric-sql/pglite-tools": "0.2.7",
|
||||||
"@hono/node-server": "1.14.2",
|
"@hono/node-server": "1.19.6",
|
||||||
"@mrleebo/prisma-ast": "0.12.1",
|
"@mrleebo/prisma-ast": "0.12.1",
|
||||||
"@prisma/get-platform": "6.8.2",
|
"@prisma/get-platform": "6.8.2",
|
||||||
"@prisma/query-plan-executor": "6.18.0",
|
"@prisma/query-plan-executor": "6.18.0",
|
||||||
"foreground-child": "3.3.1",
|
"foreground-child": "3.3.1",
|
||||||
"get-port-please": "3.1.2",
|
"get-port-please": "3.1.2",
|
||||||
"hono": "4.7.10",
|
"hono": "4.10.6",
|
||||||
"http-status-codes": "2.3.0",
|
"http-status-codes": "2.3.0",
|
||||||
"pathe": "2.0.3",
|
"pathe": "2.0.3",
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"remeda": "2.21.3",
|
"remeda": "2.21.3",
|
||||||
"std-env": "3.9.0",
|
"std-env": "3.9.0",
|
||||||
"valibot": "1.1.0",
|
"valibot": "1.2.0",
|
||||||
"zeptomatch": "2.0.2"
|
"zeptomatch": "2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2007,70 +2007,70 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.1.0.tgz",
|
||||||
"integrity": "sha512-f+D/vdKeImqUHysd5Bgv8LQ1whl4sbLepHyYMQQMK61cp4WjwJVryophleLUrfEJRpBLGTBI/7fnLVENxxMFPQ==",
|
"integrity": "sha512-KQlraOybdHAzVv45KWKJzpR9mJLkib7/TyApQpqrsL7FUHfgjIcy8jrVGt3iNfG6/GDDl+LNlJ84JSQwIfdzxA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "7.0.1",
|
"@prisma/debug": "7.1.0",
|
||||||
"@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6",
|
"@prisma/engines-version": "7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
|
||||||
"@prisma/fetch-engine": "7.0.1",
|
"@prisma/fetch-engine": "7.1.0",
|
||||||
"@prisma/get-platform": "7.0.1"
|
"@prisma/get-platform": "7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6",
|
"version": "7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba.tgz",
|
||||||
"integrity": "sha512-RA7pShKvijHib4USRB3YuLTQamHKJPkTRDc45AwxfahUQngiGVMlIj4ix4emUxkrum4o/jwn82WIwlG57EtgiQ==",
|
"integrity": "sha512-qZUevUh+yPhGT28rDQnV8V2kLnFjirzhVD67elRPIJHRsUV/mkII10HSrJrhK/U2GYgAxXR2VEREtq7AsfS8qw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines/node_modules/@prisma/debug": {
|
"node_modules/@prisma/engines/node_modules/@prisma/debug": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.1.0.tgz",
|
||||||
"integrity": "sha512-5+25XokVeAK2Z2C9W457AFw7Hk032Q3QI3G58KYKXPlpgxy+9FvV1+S1jqfJ2d4Nmq9LP/uACrM6OVhpJMSr8w==",
|
"integrity": "sha512-pPAckG6etgAsEBusmZiFwM9bldLSNkn++YuC4jCTJACdK5hLOVnOzX7eSL2FgaU6Gomd6wIw21snUX2dYroMZQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.1.0.tgz",
|
||||||
"integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==",
|
"integrity": "sha512-lq8hMdjKiZftuT5SssYB3EtQj8+YjL24/ZTLflQqzFquArKxBcyp6Xrblto+4lzIKJqnpOjfMiBjMvl7YuD7+Q==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "7.0.1"
|
"@prisma/debug": "7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.1.0.tgz",
|
||||||
"integrity": "sha512-5DnSairYIYU7dcv/9pb1KCwIRHZfhVOd34855d01lUI5QdF9rdCkMywPQbBM67YP7iCgQoEZO0/COtOMpR4i9A==",
|
"integrity": "sha512-GZYF5Q8kweXWGfn87hTu17kw7x1DgnehgKoE4Zg1BmHYF3y1Uu0QRY/qtSE4veH3g+LW8f9HKqA0tARG66bxxQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "7.0.1",
|
"@prisma/debug": "7.1.0",
|
||||||
"@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6",
|
"@prisma/engines-version": "7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
|
||||||
"@prisma/get-platform": "7.0.1"
|
"@prisma/get-platform": "7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": {
|
"node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.1.0.tgz",
|
||||||
"integrity": "sha512-5+25XokVeAK2Z2C9W457AFw7Hk032Q3QI3G58KYKXPlpgxy+9FvV1+S1jqfJ2d4Nmq9LP/uACrM6OVhpJMSr8w==",
|
"integrity": "sha512-pPAckG6etgAsEBusmZiFwM9bldLSNkn++YuC4jCTJACdK5hLOVnOzX7eSL2FgaU6Gomd6wIw21snUX2dYroMZQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.1.0.tgz",
|
||||||
"integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==",
|
"integrity": "sha512-lq8hMdjKiZftuT5SssYB3EtQj8+YjL24/ZTLflQqzFquArKxBcyp6Xrblto+4lzIKJqnpOjfMiBjMvl7YuD7+Q==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "7.0.1"
|
"@prisma/debug": "7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
@ -5539,9 +5539,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/grammex": {
|
"node_modules/grammex": {
|
||||||
"version": "3.1.11",
|
"version": "3.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz",
|
||||||
"integrity": "sha512-HNwLkgRg9SqTAd1N3Uh/MnKwTBTzwBxTOPbXQ8pb0tpwydjk90k4zRE8JUn9fMUiRwKtXFZ1TWFmms3dZHN+Fg==",
|
"integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@ -5664,9 +5664,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hono": {
|
"node_modules/hono": {
|
||||||
"version": "4.7.10",
|
"version": "4.10.6",
|
||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.7.10.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.10.6.tgz",
|
||||||
"integrity": "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==",
|
"integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -7726,16 +7726,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.1.0.tgz",
|
||||||
"integrity": "sha512-zp93MdFMSU1IHPEXbUHVUuD8wauh2BUm14OVxhxGrWJQQpXpda0rW4VSST2bci4raoldX64/wQxHKkl/wqDskQ==",
|
"integrity": "sha512-dy/3urE4JjhdiW5b09pGjVhGI7kPESK2VlCDrCqeYK5m5SslAtG5FCGnZWP7E8Sdg+Ow1wV2mhJH5RTFL5gEsw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "7.0.1",
|
"@prisma/config": "7.1.0",
|
||||||
"@prisma/dev": "0.13.0",
|
"@prisma/dev": "0.15.0",
|
||||||
"@prisma/engines": "7.0.1",
|
"@prisma/engines": "7.1.0",
|
||||||
"@prisma/studio-core": "0.8.2",
|
"@prisma/studio-core": "0.8.2",
|
||||||
"mysql2": "3.15.3",
|
"mysql2": "3.15.3",
|
||||||
"postgres": "3.4.7"
|
"postgres": "3.4.7"
|
||||||
@ -9366,9 +9366,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/valibot": {
|
"node_modules/valibot": {
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
|
||||||
"integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==",
|
"integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@prisma/adapter-better-sqlite3": "^7.0.0",
|
"@prisma/adapter-better-sqlite3": "^7.0.0",
|
||||||
"@prisma/adapter-pg": "^7.0.0",
|
"@prisma/adapter-pg": "^7.0.0",
|
||||||
"@prisma/client": "^7.0.1",
|
"@prisma/client": "^7.1.0",
|
||||||
"@zxing/browser": "^0.1.5",
|
"@zxing/browser": "^0.1.5",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"next": "16.0.3",
|
"next": "16.0.3",
|
||||||
@ -42,7 +42,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.0.3",
|
"eslint-config-next": "16.0.3",
|
||||||
"prisma": "^7.0.1",
|
"prisma": "^7.1.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "avatarUrl" TEXT;
|
||||||
@ -24,6 +24,7 @@ model User {
|
|||||||
groupId String?
|
groupId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
avatarUrl String?
|
||||||
|
|
||||||
devicesCreated Device[] @relation("DeviceCreatedBy")
|
devicesCreated Device[] @relation("DeviceCreatedBy")
|
||||||
devicesUpdated Device[] @relation("DeviceUpdatedBy")
|
devicesUpdated Device[] @relation("DeviceUpdatedBy")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user