'use client' import { useCallback, useEffect, useState } from 'react' import { useLocale, useTranslations } from 'next-intl' import { useRouter, useSearchParams } from 'next/navigation' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Switch } from '@/components/ui/switch' import { Skeleton } from '@/components/ui/skeleton' import { useToast } from '@/components/ui/use-toast' import { useFormat } from '@/lib/hooks/use-format' import { failureDescription } from '@/lib/browser/action-failure' import type { ErrorLocale } from '@/lib/errors/get-error-message' import { CreditCard, Link2, Loader2, RefreshCw, Unlink } from 'lucide-react' import { zettleRequest, syncSummary, ZETTLE_CONNECT_TIMEOUT_MS, ZETTLE_SYNC_TIMEOUT_MS, type ZettleSyncPayload, } from '../lib/settings-actions' import type { ZettleStatusResponse } from '../types' type ConnectionInfo = NonNullable const STATUS_VARIANT: Record = { active: 'success', pending: 'secondary', revoked: 'warning', error: 'destructive', } export default function ZettleSettingsPanel() { const t = useTranslations('zettle') const tCommon = useTranslations('common') const locale = useLocale() as ErrorLocale const { toast } = useToast() const router = useRouter() const searchParams = useSearchParams() const { formatDateLong } = useFormat() const [loading, setLoading] = useState(true) const [loadFailed, setLoadFailed] = useState(false) const [configured, setConfigured] = useState(false) const [connection, setConnection] = useState(null) const [connecting, setConnecting] = useState(false) const [disconnecting, setDisconnecting] = useState(false) const [confirmDisconnect, setConfirmDisconnect] = useState(false) const [syncing, setSyncing] = useState(false) const [togglingTransactionSync, setTogglingTransactionSync] = useState(false) const failureCopy = { timeout: t('action_timeout'), network: t('action_network') } const loadStatus = useCallback(async () => { const result = await zettleRequest({ url: '/api/extensions/ext/zettle/status', method: 'GET', locale, }) setLoading(false) if (!result.ok || !result.data) { setLoadFailed(true) return } setLoadFailed(false) setConfigured(result.data.configured) setConnection(result.data.connection) }, [locale]) useEffect(() => { void loadStatus() }, [loadStatus]) useEffect(() => { const connected = searchParams.get('zettle_connected') const error = searchParams.get('zettle_error') if (!connected && !error) return if (connected === 'true') { toast({ title: t('connected_toast_title'), description: t('connected_toast_description') }) } else if (error) { // searchParams.get already returns the decoded value; decoding again // throws URIError when the message contains a literal % character. toast({ title: t('connect_failed_title'), description: error, variant: 'destructive', }) } router.replace('/import?mode=zettle') }, [searchParams, toast, t, router]) function retryLoadStatus() { setLoading(true) void loadStatus() } async function handleConnect() { if (connecting) return setConnecting(true) try { const result = await zettleRequest<{ url?: string }>({ url: '/api/extensions/ext/zettle/connect', locale, timeoutMs: ZETTLE_CONNECT_TIMEOUT_MS, }) if (!result.ok) { toast({ title: t('connect_failed_title'), description: failureDescription(result, failureCopy), variant: 'destructive', }) return } if (!result.data?.url) { toast({ title: t('connect_failed_title'), variant: 'destructive' }) return } window.location.href = result.data.url } finally { setConnecting(false) } } async function handleSyncNow() { if (syncing) return setSyncing(true) try { const result = await zettleRequest({ url: '/api/extensions/ext/zettle/sync', locale, timeoutMs: ZETTLE_SYNC_TIMEOUT_MS, }) if (!result.ok) { toast({ title: t('sync_failed_title'), description: failureDescription(result, failureCopy), variant: 'destructive', }) return } const summary = syncSummary(result.data) if (summary.reason === 'revoked') { toast({ title: t('sync_failed_title'), description: t('sync_revoked'), variant: 'destructive', }) } else if (summary.reason === 'partial') { toast({ title: t('sync_partial_title'), description: t('sync_partial', summary.values) }) } else if (summary.reason === 'empty') { toast({ title: t('sync_done_title'), description: t('sync_done_empty') }) } else if (summary.reason === 'errors') { toast({ title: t('sync_done_title'), description: t('sync_done_feed_errors', summary.values) }) } else if (summary.reason === 'feed') { toast({ title: t('sync_done_title'), description: t('sync_done_feed', summary.values) }) } else { toast({ title: t('sync_done_title') }) } await loadStatus() } finally { setSyncing(false) } } async function handleToggleTransactionSync(enabled: boolean) { if (togglingTransactionSync) return setTogglingTransactionSync(true) try { const result = await zettleRequest({ url: '/api/extensions/ext/zettle/transaction-sync', body: { enabled }, locale, }) if (!result.ok) { toast({ title: t('transaction_sync_toggle_failed'), description: failureDescription(result, failureCopy), variant: 'destructive', }) return } toast({ title: enabled ? t('transaction_sync_enabled_toast') : t('transaction_sync_disabled_toast'), }) await loadStatus() } finally { setTogglingTransactionSync(false) } } async function handleDisconnect() { if (!connection || disconnecting) return setDisconnecting(true) try { const result = await zettleRequest({ url: '/api/extensions/ext/zettle/disconnect', method: 'DELETE', body: { connection_id: connection.id }, locale, }) if (!result.ok) { toast({ title: t('disconnect_failed_title'), description: failureDescription(result, failureCopy), variant: 'destructive', }) return } toast({ title: t('disconnected_toast_title'), description: t('disconnected_toast_description') }) setConfirmDisconnect(false) await loadStatus() } finally { setDisconnecting(false) } } if (loading) { return ( ) } if (loadFailed) { return ( {t('title')}

{t('load_failed')}

) } if (!configured) { return ( {t('title')}

{t('not_configured')}

) } const isActive = connection?.status === 'active' const showConnect = !connection || !isActive return ( {t('title')}

{t('description')}

{connection && (
{connection.organization_name || connection.organization_uuid || t('unnamed_store')} {t(`status_${connection.status}`)}
{connection.organization_uuid && connection.organization_name && (

{connection.organization_uuid}

)} {isActive && connection.connected_at && (

{t('connected_since', { date: formatDateLong(connection.connected_at) })}

)} {connection.error_message && (

{connection.error_message}

)}
{isActive && (confirmDisconnect ? (
) : (
))}
)} {showConnect && (

{t('connect_hint')}

)} {isActive && connection && (

{t('transaction_sync_title')}

{t('transaction_sync_description')}

{connection.transaction_sync_enabled ? (

{connection.last_order_synced_at ? t('transaction_sync_last_synced', { date: formatDateLong(connection.last_order_synced_at), }) : t('transaction_sync_never_synced')}

) : (

{t('transaction_sync_backfill_note')}

)}
)}
) }