diff --git a/__tests__/components/mobile/logout-button.test.tsx b/__tests__/components/mobile/logout-button.test.tsx new file mode 100644 index 0000000..bffa8fe --- /dev/null +++ b/__tests__/components/mobile/logout-button.test.tsx @@ -0,0 +1,46 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' + +const { logoutMock } = vi.hoisted(() => ({ + logoutMock: vi.fn().mockResolvedValue(undefined), +})) +vi.mock('@/actions/auth', () => ({ logoutAction: logoutMock })) + +import { LogoutButton } from '@/components/mobile/logout-button' + +beforeEach(() => { + logoutMock.mockClear() +}) + +describe('LogoutButton', () => { + it('toont initieel alleen de Uitloggen-knop, geen dialog', () => { + render() + expect(screen.getByRole('button', { name: /Uitloggen/ })).toBeTruthy() + expect(screen.queryByText(/Weet je zeker/)).toBeNull() + }) + + it('opent AlertDialog bij klikken op de knop', () => { + render() + fireEvent.click(screen.getByRole('button', { name: /Uitloggen/ })) + expect(screen.getByText('Uitloggen?')).toBeTruthy() + expect(screen.getByText(/Weet je zeker/)).toBeTruthy() + }) + + it('roept logoutAction aan op bevestigen', async () => { + const { container } = render() + fireEvent.click(screen.getByRole('button', { name: /Uitloggen/ })) + // Het body-portal wordt buiten container gerenderd; query op document.body. + const allButtons = Array.from(document.body.querySelectorAll('button')) + const confirmBtn = allButtons.find(b => b.textContent?.trim() === 'Uitloggen' && !container.contains(b)) ?? allButtons[allButtons.length - 1] + fireEvent.click(confirmBtn) + await waitFor(() => expect(logoutMock).toHaveBeenCalledTimes(1)) + }) + + it('roept logoutAction NIET aan bij annuleren', () => { + render() + fireEvent.click(screen.getByRole('button', { name: /Uitloggen/ })) + fireEvent.click(screen.getByText('Annuleren')) + expect(logoutMock).not.toHaveBeenCalled() + }) +}) diff --git a/app/(mobile)/m/settings/page.tsx b/app/(mobile)/m/settings/page.tsx new file mode 100644 index 0000000..d1a7070 --- /dev/null +++ b/app/(mobile)/m/settings/page.tsx @@ -0,0 +1,92 @@ +// PBI-11 / ST-1136: Mobile Settings — read-only account, product-selector, +// QR-pairing-instructie, logout. Eigenlijke productactivering loopt via de +// bestaande setActiveProductAction (ActivateProductButton). + +import Link from 'next/link' +import { prisma } from '@/lib/prisma' +import { productAccessFilter } from '@/lib/product-access' +import { requireSession } from '@/lib/auth-guard' +import { ActivateProductButton } from '@/components/shared/activate-product-button' +import { LogoutButton } from '@/components/mobile/logout-button' +import { Badge } from '@/components/ui/badge' + +export const metadata = { + title: 'Settings', +} + +export default async function MobileSettingsPage() { + const session = await requireSession() + + const [user, products] = await Promise.all([ + prisma.user.findUnique({ + where: { id: session.userId }, + select: { username: true, is_demo: true, active_product_id: true }, + }), + prisma.product.findMany({ + where: { archived: false, ...productAccessFilter(session.userId) }, + orderBy: { name: 'asc' }, + select: { id: true, name: true }, + }), + ]) + + const isDemo = user?.is_demo ?? false + + return ( +
+

Settings

+ +
+

Account

+
+ {user?.username ?? '—'} + {isDemo && ( + Demo + )} +
+
+ +
+

Actief product

+ {products.length === 0 ? ( +

Geen producten beschikbaar.

+ ) : ( +
    + {products.map((p) => { + const active = p.id === user?.active_product_id + return ( +
  • +
    + {p.name} + {active && ( + Actief + )} +
    + {!active && ( + + )} +
  • + ) + })} +
+ )} +
+ +
+

Inloggen op desktop

+

+ Open scrum4me.app/login op je desktop om in te loggen via QR-code. QR-pairing start vanaf de desktop. +

+
+ +
+

Uitloggen

+ +
+
+ ) +} diff --git a/components/mobile/logout-button.tsx b/components/mobile/logout-button.tsx new file mode 100644 index 0000000..82f0819 --- /dev/null +++ b/components/mobile/logout-button.tsx @@ -0,0 +1,56 @@ +'use client' + +import { useState, useTransition } from 'react' +import { LogOut } from 'lucide-react' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { Button } from '@/components/ui/button' +import { logoutAction } from '@/actions/auth' + +export function LogoutButton() { + const [open, setOpen] = useState(false) + const [pending, startTransition] = useTransition() + + function confirm() { + startTransition(async () => { + await logoutAction() + }) + } + + return ( + <> + + + + + Uitloggen? + + Weet je zeker dat je wilt uitloggen? + + + + setOpen(false)}>Annuleren + + {pending ? 'Bezig…' : 'Uitloggen'} + + + + + + ) +}