This commit is contained in:
@@ -27,7 +27,7 @@ export default function DashboardPage() {
|
||||
|
||||
const deviceQuery = useQuery({
|
||||
queryKey: ['device', venueId],
|
||||
queryFn: () => devicesApi.get(venueId),
|
||||
queryFn: () => devicesApi.list(venueId),
|
||||
// A terminal going offline sends no event, so its status is always polled
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
@@ -45,7 +45,12 @@ export default function DashboardPage() {
|
||||
});
|
||||
|
||||
const tabs = tabsQuery.data?.tabs ?? [];
|
||||
const device = deviceQuery.data?.device;
|
||||
const devices = deviceQuery.data?.devices ?? [];
|
||||
// The terminal heard from most recently
|
||||
const device = devices.reduce<(typeof devices)[number] | undefined>(
|
||||
(latest, d) => (!latest || d.lastSeenAt > latest.lastSeenAt ? d : latest),
|
||||
undefined,
|
||||
);
|
||||
const commands = recentQuery.data?.commands ?? [];
|
||||
const outstanding = tabs.reduce((sum, tab) => sum + tab.totalInCents, 0);
|
||||
const isLoading = tabsQuery.isLoading || deviceQuery.isLoading || pendingQuery.isLoading;
|
||||
@@ -88,7 +93,11 @@ export default function DashboardPage() {
|
||||
<span className="text-2xl font-bold truncate">{device.name}</span>
|
||||
<DeviceStatusBadge lastSeenAt={device.lastSeenAt} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t('overview.last_seen', { time: relativeTime(device.lastSeenAt) })}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{devices.length > 1
|
||||
? t('overview.terminals', { count: devices.length, time: relativeTime(device.lastSeenAt) })
|
||||
: t('overview.last_seen', { time: relativeTime(device.lastSeenAt) })}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-2xl font-bold">{t('overview.not_connected')}</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { devicesApi, venuesApi } from '@/lib/api/client';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { devicesApi, venuesApi, type Device } from '@/lib/api/client';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DeviceStatusBadge } from '@/components/device-status-badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { toast } from '@/components/ui/toast';
|
||||
import { useLang } from '@/lib/i18n';
|
||||
import { useSession } from '@/lib/session';
|
||||
@@ -17,6 +18,8 @@ export default function SettingsPage() {
|
||||
const { venueId, role } = useSession();
|
||||
const [regDialog, setRegDialog] = useState<{ open: boolean; code: string; expiresAt: string } | null>(null);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [unpairing, setUnpairing] = useState<Device | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const { t, te, relativeTime, dateTime, time } = useLang();
|
||||
|
||||
const { data: venue } = useQuery({
|
||||
@@ -26,12 +29,22 @@ export default function SettingsPage() {
|
||||
|
||||
const { data: deviceData } = useQuery({
|
||||
queryKey: ['device', venueId],
|
||||
queryFn: () => devicesApi.get(venueId),
|
||||
queryFn: () => devicesApi.list(venueId),
|
||||
// A terminal going offline sends no event, so its status is always polled
|
||||
refetchInterval: 15000,
|
||||
});
|
||||
|
||||
const device = deviceData?.device;
|
||||
const devices = deviceData?.devices ?? [];
|
||||
|
||||
const unpairMutation = useMutation({
|
||||
mutationFn: (device: Device) => devicesApi.unpair(venueId, device.id),
|
||||
onSuccess: (_, device) => toast({ title: t('settings.unpaired', { name: device.name }), variant: 'success' }),
|
||||
onError: (err) => toast({ title: t('common.failed'), description: te(err), variant: 'destructive' }),
|
||||
onSettled: () => {
|
||||
setUnpairing(null);
|
||||
queryClient.invalidateQueries({ queryKey: ['device'] });
|
||||
},
|
||||
});
|
||||
|
||||
async function handleGenerateCode() {
|
||||
setGenerating(true);
|
||||
@@ -71,17 +84,29 @@ export default function SettingsPage() {
|
||||
<CardHeader>
|
||||
<CardTitle>{t('settings.device')}</CardTitle>
|
||||
<CardDescription>
|
||||
{device
|
||||
? t('settings.paired', { name: device.name, time: relativeTime(device.lastSeenAt) })
|
||||
: t('settings.not_paired')}
|
||||
{devices.length === 0
|
||||
? t('settings.not_paired')
|
||||
: devices.length === 1
|
||||
? t('settings.paired', { name: devices[0].name, time: relativeTime(devices[0].lastSeenAt) })
|
||||
: t('settings.terminals', { count: devices.length })}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{device ? (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Monitor className="h-4 w-4 text-muted-foreground" />
|
||||
<DeviceStatusBadge lastSeenAt={device.lastSeenAt} />
|
||||
<span className="text-muted-foreground">{t('settings.since', { date: dateTime(device.createdAt) })}</span>
|
||||
{devices.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{devices.map((device) => (
|
||||
<div key={device.id} className="flex flex-wrap items-center gap-2 text-sm">
|
||||
<Monitor className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium">{device.name}</span>
|
||||
<DeviceStatusBadge lastSeenAt={device.lastSeenAt} />
|
||||
<span className="text-muted-foreground">{t('settings.since', { date: dateTime(device.createdAt) })}</span>
|
||||
{role === 'admin' && (
|
||||
<Button variant="ghost" size="sm" className="ml-auto" onClick={() => setUnpairing(device)}>
|
||||
{t('settings.unpair')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
@@ -91,7 +116,7 @@ export default function SettingsPage() {
|
||||
{role === 'admin' && (
|
||||
<Button onClick={handleGenerateCode} disabled={generating} size="sm" className="mt-3">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{generating ? t('settings.generating') : device ? t('settings.repair') : t('settings.pair')}
|
||||
{generating ? t('settings.generating') : devices.length > 0 ? t('settings.repair') : t('settings.pair')}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -99,6 +124,18 @@ export default function SettingsPage() {
|
||||
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={unpairing !== null}
|
||||
onOpenChange={(open) => !open && setUnpairing(null)}
|
||||
title={t('settings.unpair_title', { name: unpairing?.name ?? '' })}
|
||||
description={t('settings.unpair_desc')}
|
||||
confirmLabel={t('settings.unpair')}
|
||||
cancelLabel={t('common.cancel')}
|
||||
variant="destructive"
|
||||
loading={unpairMutation.isPending}
|
||||
onConfirm={() => unpairing && unpairMutation.mutate(unpairing)}
|
||||
/>
|
||||
|
||||
{regDialog && (
|
||||
<Dialog open={regDialog.open} onOpenChange={(open) => !open && setRegDialog(null)}>
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { commandsApi, plannedTabsApi, remindersApi, tabsApi, type Tab, type TabLine } from '@/lib/api/client';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -40,7 +40,6 @@ export default function TabsPage() {
|
||||
const [closeTab, setCloseTab] = useState<Tab | null>(null);
|
||||
const [paymentMethod, setPaymentMethod] = useState('');
|
||||
const [expandedTabs, setExpandedTabs] = useState<Set<string>>(new Set());
|
||||
const [watchedCommandIds, setWatchedCommandIds] = useState<Set<string>>(new Set());
|
||||
const [search, setSearch] = useState('');
|
||||
const [sort, setSort] = useState<SortKey>('name');
|
||||
const [plannedName, setPlannedName] = useState('');
|
||||
@@ -61,14 +60,6 @@ export default function TabsPage() {
|
||||
});
|
||||
const closingTabIds = new Set((pendingQuery.data?.commands ?? []).map((c) => c.tabId));
|
||||
|
||||
// Only needed to report the outcome of close requests sent from this page
|
||||
const resultsQuery = useQuery({
|
||||
queryKey: ['commands', venueId, 'results'],
|
||||
queryFn: () => commandsApi.list(venueId, { limit: 50 }),
|
||||
enabled: watchedCommandIds.size > 0,
|
||||
refetchInterval: live ? false : 3000,
|
||||
});
|
||||
|
||||
const plannedQuery = useQuery({
|
||||
queryKey: ['planned-tabs', venueId],
|
||||
queryFn: () => plannedTabsApi.list(venueId),
|
||||
@@ -78,31 +69,16 @@ export default function TabsPage() {
|
||||
// Tiles only show customers the terminal has not opened a tab for yet
|
||||
const pendingPlanned = (plannedQuery.data?.plannedTabs ?? []).filter((pt) => !pt.deliveredAt);
|
||||
|
||||
useEffect(() => {
|
||||
const finished = (resultsQuery.data?.commands ?? []).filter((c) => watchedCommandIds.has(c.id) && c.status !== 'pending');
|
||||
if (finished.length === 0) return;
|
||||
for (const cmd of finished) {
|
||||
const status = t(`status.${cmd.status}`);
|
||||
toast({
|
||||
title: status,
|
||||
description: t('tabs.request_result', { name: cmd.customerName ?? cmd.tabId.substring(0, 8), status }),
|
||||
variant: cmd.status === 'completed' ? 'success' : 'destructive',
|
||||
});
|
||||
}
|
||||
setWatchedCommandIds((current) => {
|
||||
const next = new Set(current);
|
||||
for (const cmd of finished) next.delete(cmd.id);
|
||||
return next;
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['tabs'] });
|
||||
}, [resultsQuery.data, watchedCommandIds, queryClient, t]);
|
||||
|
||||
const createCommandMutation = useMutation({
|
||||
mutationFn: ({ tab, paymentMethod }: { tab: Tab; paymentMethod: string }) =>
|
||||
commandsApi.create(venueId, { tabId: tab.tabId, expectedVersion: tab.version, paymentMethod, type: 'close_tab' }),
|
||||
onSuccess: (result) => {
|
||||
setWatchedCommandIds((current) => new Set(current).add(result.id));
|
||||
toast({ title: t('tabs.request_sent'), description: result.warning || t('tabs.request_sent_desc'), variant: result.warning ? 'default' : 'success' });
|
||||
onSuccess: (result, { tab }) => {
|
||||
toast({
|
||||
title: t('tabs.settled'),
|
||||
description: t('tabs.settled_desc', { name: tab.customerName, amount: formatCents(result.amountInCents) }),
|
||||
variant: 'success',
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['tabs'] });
|
||||
},
|
||||
onError: (err) => {
|
||||
toast({ title: t('common.failed'), description: te(err), variant: 'destructive' });
|
||||
@@ -325,7 +301,7 @@ export default function TabsPage() {
|
||||
</TableCell>
|
||||
<TableCell className="tabular-nums">{formatCents(tab.totalInCents)}</TableCell>
|
||||
<TableCell>{tab.itemCount}</TableCell>
|
||||
<TableCell>{relativeTime(tab.lastReportedAt)}</TableCell>
|
||||
<TableCell>{relativeTime(tab.lastChangedAt)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{closingTabIds.has(tab.tabId) ? (
|
||||
<Badge variant="warning">{t('tabs.closing')}</Badge>
|
||||
|
||||
@@ -9,6 +9,7 @@ export type Role = 'admin' | 'viewer';
|
||||
|
||||
export type TabLine = { productName: string; quantity: number; unitPriceInCents: number };
|
||||
|
||||
/** An open tab as the server holds it: every terminal of the venue shows the same. */
|
||||
export interface Tab {
|
||||
tabId: string;
|
||||
customerName: string;
|
||||
@@ -16,11 +17,7 @@ export interface Tab {
|
||||
totalInCents: number;
|
||||
itemCount: number;
|
||||
items: TabLine[] | null;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
deviceLastSeenAt: string;
|
||||
deviceRevokedAt: string | null;
|
||||
lastReportedAt: string;
|
||||
lastChangedAt: string;
|
||||
plannedTabId: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
@@ -40,7 +37,8 @@ export interface Command {
|
||||
createdAt: string;
|
||||
processedAt: string | null;
|
||||
result: string | null;
|
||||
deviceId: string;
|
||||
/** Null for a tab the server settled itself */
|
||||
deviceId: string | null;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
@@ -259,8 +257,10 @@ export const tabsApi = {
|
||||
|
||||
// Devices API
|
||||
export const devicesApi = {
|
||||
get: (venueId: string) =>
|
||||
api<{ device: Device | null }>(`/venues/${venueId}/devices`),
|
||||
list: (venueId: string) =>
|
||||
api<{ devices: Device[] }>(`/venues/${venueId}/devices`),
|
||||
unpair: (venueId: string, deviceId: string) =>
|
||||
api(`/venues/${venueId}/commands/devices/${deviceId}/revoke`, { method: 'POST' }),
|
||||
generateCode: () => api<{ code: string; expiresAt: string }>('/devices/generation-code'),
|
||||
};
|
||||
|
||||
@@ -285,7 +285,7 @@ export const commandsApi = {
|
||||
);
|
||||
},
|
||||
create: (venueId: string, data: { tabId: string; type: string; expectedVersion: number; paymentMethod: string }) =>
|
||||
api<{ id: string; status: string; warning?: string }>(`/venues/${venueId}/commands`, {
|
||||
api<{ id: string; status: string; amountInCents: number }>(`/venues/${venueId}/commands`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
@@ -29,6 +29,8 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'errors.version_mismatch': 'The tab changed since it was loaded. Check it and try again.',
|
||||
'errors.command_already_pending': 'A close request for this tab is already waiting for the terminal',
|
||||
'errors.tab_not_owned': 'This tab is no longer open on the terminal',
|
||||
'errors.tab_not_found': 'This tab is no longer open',
|
||||
'errors.tab_empty': 'There is nothing on this tab to settle',
|
||||
'errors.device_revoked': 'The terminal for this tab has been unpaired',
|
||||
'errors.name_exists': 'This customer already exists',
|
||||
'errors.email_exists': 'A user with this email already exists',
|
||||
@@ -90,14 +92,13 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'tabs.lines_empty': 'No items on this tab.',
|
||||
'tabs.each': 'each',
|
||||
'tabs.close_title': "Close {name}'s tab?",
|
||||
'tabs.close_desc': 'Send a close request to the terminal.',
|
||||
'tabs.close_desc': 'Settles everything on the tab now. The terminals show it after their next sync.',
|
||||
'tabs.payment': 'Payment method',
|
||||
'tabs.select_payment': 'Please select a payment method',
|
||||
'tabs.send': 'Send close request',
|
||||
'tabs.sending': 'Sending...',
|
||||
'tabs.request_sent': 'Close request sent',
|
||||
'tabs.request_sent_desc': 'Sent to the terminal',
|
||||
'tabs.request_result': 'Close request for {name}: {status}',
|
||||
'tabs.settled': 'Tab settled',
|
||||
'tabs.settled_desc': '{name}: {amount}',
|
||||
'tabs.send_test': 'Send test email',
|
||||
'tabs.test_sent': 'Test email sent',
|
||||
'tabs.test_sent_desc': 'Sent to {email}',
|
||||
@@ -185,7 +186,12 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'settings.since': 'Since {date}',
|
||||
'settings.pair_desc': 'Pair a bar terminal to start receiving tabs.',
|
||||
'settings.pair': 'Pair Terminal',
|
||||
'settings.repair': 'Re-pair Terminal',
|
||||
'settings.repair': 'Pair Another Terminal',
|
||||
'settings.unpair': 'Unpair',
|
||||
'settings.unpair_title': 'Unpair {name}?',
|
||||
'settings.unpair_desc': 'The terminal stops syncing and has to be paired again to reconnect. Its changes that have not been synced yet are lost.',
|
||||
'settings.unpaired': '{name} is unpaired',
|
||||
'settings.terminals': '{count} terminals paired',
|
||||
'settings.generating': 'Generating...',
|
||||
'settings.pair_title': 'Pair a Terminal',
|
||||
'settings.pair_instructions': 'Enter this code on the bar terminal to connect it to your venue.',
|
||||
@@ -208,6 +214,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'overview.no_commands': 'No commands yet',
|
||||
'overview.not_connected': 'Not connected',
|
||||
'overview.last_seen': 'Last seen {time}',
|
||||
'overview.terminals': '{count} terminals, last seen {time}',
|
||||
'auth.login': 'Sign in',
|
||||
'auth.logging_in': 'Signing in...',
|
||||
'auth.email': 'Email',
|
||||
@@ -293,27 +300,27 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'stock.out': 'Out',
|
||||
'stock.adjust': 'Adjust',
|
||||
'stock.start_tracking': 'Track stock',
|
||||
'stock.start_tracking_desc': "The terminal doesn't track stock for this product yet. Enter how many you have to start tracking it.",
|
||||
'stock.start_tracking_desc': "Stock isn't tracked for this product yet. Enter how many you have to start tracking it.",
|
||||
'stock.cancel_adjustment': 'Cancel {change} for {name}',
|
||||
'stock.cancelled': 'Change cancelled',
|
||||
'stock.current': 'Currently {stock} in stock on the terminal.',
|
||||
'stock.current': 'Currently {stock} in stock.',
|
||||
'stock.mode_add': 'Add (delivery)',
|
||||
'stock.mode_remove': 'Take out (breakage, own use…)',
|
||||
'stock.mode_count': 'Count (set the stock to what you counted)',
|
||||
'stock.quantity': 'Quantity',
|
||||
'stock.counted': 'Counted',
|
||||
'stock.preview': '{from} → {to}',
|
||||
'stock.count_hint': 'Drinks sold between your count and the terminal receiving it are not subtracted.',
|
||||
'stock.count_hint': 'Drinks rung up before your count are in it: they are not subtracted again, also not when an offline terminal sends them later.',
|
||||
'stock.note': 'Note (optional)',
|
||||
'stock.note_placeholder': 'e.g. delivery from the brewery',
|
||||
'stock.threshold': 'Warn when stock is at or below',
|
||||
'stock.threshold_none': 'No warning',
|
||||
'stock.threshold_save': 'Save',
|
||||
'stock.threshold_saved': 'Warning level saved',
|
||||
'stock.send': 'Send to terminal',
|
||||
'stock.send': 'Save',
|
||||
'stock.sending': 'Sending…',
|
||||
'stock.sent': 'Sent to the terminal',
|
||||
'stock.sent_desc': 'The stock changes on the terminal at its next sync.',
|
||||
'stock.sent': 'Stock updated',
|
||||
'stock.sent_desc': 'The terminals show it after their next sync.',
|
||||
'stock.delivery': 'Register delivery',
|
||||
'stock.delivery_title': 'Register delivery',
|
||||
'stock.delivery_desc': 'Enter how many of each product were delivered. Products you leave empty are not changed.',
|
||||
@@ -355,6 +362,8 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'errors.version_mismatch': 'De poef is gewijzigd sinds ze geladen werd. Controleer en probeer opnieuw.',
|
||||
'errors.command_already_pending': 'Er wacht al een sluitverzoek voor deze poef op de kassa',
|
||||
'errors.tab_not_owned': 'Deze poef staat niet meer open op de kassa',
|
||||
'errors.tab_not_found': 'Deze poef staat niet meer open',
|
||||
'errors.tab_empty': 'Er staat niets op deze poef om af te rekenen',
|
||||
'errors.device_revoked': 'De kassa van deze poef is ontkoppeld',
|
||||
'errors.name_exists': 'Deze klant bestaat al',
|
||||
'errors.email_exists': 'Er bestaat al een gebruiker met dit e-mailadres',
|
||||
@@ -416,14 +425,13 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'tabs.lines_empty': 'Geen items op deze poef.',
|
||||
'tabs.each': 'per stuk',
|
||||
'tabs.close_title': 'Poef van {name} sluiten?',
|
||||
'tabs.close_desc': 'Stuur een verzoek naar de terminal om deze poef te sluiten.',
|
||||
'tabs.close_desc': 'Rekent alles op de poef nu af. De terminals tonen het na hun volgende synchronisatie.',
|
||||
'tabs.payment': 'Betaalmethode',
|
||||
'tabs.select_payment': 'Kies een betaalmethode',
|
||||
'tabs.send': 'Sluitverzoek versturen',
|
||||
'tabs.sending': 'Bezig met versturen...',
|
||||
'tabs.request_sent': 'Sluitverzoek verstuurd',
|
||||
'tabs.request_sent_desc': 'Verstuurd naar de terminal',
|
||||
'tabs.request_result': 'Sluitverzoek voor {name}: {status}',
|
||||
'tabs.settled': 'Poef afgerekend',
|
||||
'tabs.settled_desc': '{name}: {amount}',
|
||||
'tabs.send_test': 'Testmail versturen',
|
||||
'tabs.test_sent': 'Testmail verstuurd',
|
||||
'tabs.test_sent_desc': 'Verstuurd naar {email}',
|
||||
@@ -511,7 +519,12 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'settings.since': 'Sinds {date}',
|
||||
'settings.pair_desc': 'Koppel een barterminal om poefs te ontvangen.',
|
||||
'settings.pair': 'Terminal koppelen',
|
||||
'settings.repair': 'Terminal opnieuw koppelen',
|
||||
'settings.repair': 'Nog een terminal koppelen',
|
||||
'settings.unpair': 'Ontkoppelen',
|
||||
'settings.unpair_title': '{name} ontkoppelen?',
|
||||
'settings.unpair_desc': 'De terminal synchroniseert niet meer en moet opnieuw gekoppeld worden. Wijzigingen die nog niet gesynchroniseerd zijn, gaan verloren.',
|
||||
'settings.unpaired': '{name} is ontkoppeld',
|
||||
'settings.terminals': '{count} terminals gekoppeld',
|
||||
'settings.generating': 'Code aanmaken...',
|
||||
'settings.pair_title': 'Terminal koppelen',
|
||||
'settings.pair_instructions': 'Voer deze code in op de barterminal om hem met je zaak te verbinden.',
|
||||
@@ -534,6 +547,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'overview.no_commands': 'Nog geen opdrachten',
|
||||
'overview.not_connected': 'Niet verbonden',
|
||||
'overview.last_seen': 'Laatst gezien {time}',
|
||||
'overview.terminals': '{count} terminals, laatst gezien {time}',
|
||||
'auth.login': 'Inloggen',
|
||||
'auth.logging_in': 'Bezig met inloggen...',
|
||||
'auth.email': 'E-mail',
|
||||
@@ -619,27 +633,27 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'stock.out': 'Op',
|
||||
'stock.adjust': 'Aanpassen',
|
||||
'stock.start_tracking': 'Voorraad bijhouden',
|
||||
'stock.start_tracking_desc': 'De kassa houdt voor dit product nog geen voorraad bij. Vul in hoeveel je er hebt om te beginnen.',
|
||||
'stock.start_tracking_desc': 'Voor dit product wordt nog geen voorraad bijgehouden. Vul in hoeveel je er hebt om te beginnen.',
|
||||
'stock.cancel_adjustment': '{change} voor {name} annuleren',
|
||||
'stock.cancelled': 'Wijziging geannuleerd',
|
||||
'stock.current': 'Nu {stock} in voorraad op de kassa.',
|
||||
'stock.current': 'Nu {stock} in voorraad.',
|
||||
'stock.mode_add': 'Bijvullen (levering)',
|
||||
'stock.mode_remove': 'Afboeken (breuk, eigen gebruik…)',
|
||||
'stock.mode_count': 'Tellen (voorraad wordt wat je telde)',
|
||||
'stock.quantity': 'Aantal',
|
||||
'stock.counted': 'Geteld',
|
||||
'stock.preview': '{from} → {to}',
|
||||
'stock.count_hint': 'Wat verkocht wordt tussen je telling en het moment dat de kassa ze ontvangt, wordt niet afgetrokken.',
|
||||
'stock.count_hint': 'Wat voor je telling aangeslagen werd, zit erin: het wordt niet nog eens afgetrokken, ook niet als een offline kassa het later doorstuurt.',
|
||||
'stock.note': 'Notitie (optioneel)',
|
||||
'stock.note_placeholder': 'bv. levering brouwerij',
|
||||
'stock.threshold': 'Waarschuw vanaf',
|
||||
'stock.threshold_none': 'Geen waarschuwing',
|
||||
'stock.threshold_save': 'Opslaan',
|
||||
'stock.threshold_saved': 'Waarschuwingsniveau opgeslagen',
|
||||
'stock.send': 'Naar kassa sturen',
|
||||
'stock.send': 'Opslaan',
|
||||
'stock.sending': 'Versturen…',
|
||||
'stock.sent': 'Naar de kassa gestuurd',
|
||||
'stock.sent_desc': 'De voorraad wijzigt op de kassa bij de volgende synchronisatie.',
|
||||
'stock.sent': 'Voorraad bijgewerkt',
|
||||
'stock.sent_desc': 'De kassa\'s tonen het na hun volgende synchronisatie.',
|
||||
'stock.delivery': 'Levering invoeren',
|
||||
'stock.delivery_title': 'Levering invoeren',
|
||||
'stock.delivery_desc': 'Vul in hoeveel er van elk product geleverd is. Lege producten blijven ongewijzigd.',
|
||||
|
||||
Reference in New Issue
Block a user