feat(MemberOrders): implement member orders feature

- Added new models and routes for managing member orders and order history.
- Updated server.js to include member order routes and sync functionality.
- Enhanced frontend with new components and dialogs for viewing and managing orders.
- Integrated internationalization support for order-related texts across multiple languages.
- Updated navigation and views to include access to the new orders feature, improving user experience.
This commit is contained in:
Torsten Schulz (local)
2026-03-24 17:01:57 +01:00
parent e55ee0f88a
commit 02f1bed452
32 changed files with 1743 additions and 0 deletions

View File

@@ -22,6 +22,10 @@
<span class="dropdown-icon">🏓</span>
{{ $t('navigation.clickTtAccount') }}
</button>
<router-link to="/orders" class="dropdown-item" @click="userDropdownOpen = false">
<span class="dropdown-icon">📦</span>
{{ $t('navigation.orders') }}
</router-link>
<button v-if="canManagePermissions" type="button" class="dropdown-item" @click="openUserMenuDialog('PermissionsView', $t('navigation.permissions'))">
<span class="dropdown-icon">🔐</span>
{{ $t('navigation.permissions') }}

View File

@@ -80,6 +80,7 @@
>
📄
</span>
<span class="pointer" :title="$t('orders.title')" @click.stop="$emit('open-orders', member)">📦</span>
<span class="pointer" @click="$emit('open-tags', member)"></span>
</div>
</li>
@@ -136,6 +137,7 @@ export default {
'open-notes',
'show-pic',
'mark-form',
'open-orders',
'open-tags',
'open-quick-add',
'open-gallery'

View File

@@ -0,0 +1,56 @@
<template>
<BaseDialog
:model-value="modelValue"
:title="dialogTitle"
size="large"
:width="1220"
max-width="96vw"
@update:model-value="$emit('update:modelValue', $event)"
@close="$emit('close')"
>
<OrdersPanel :club-id="clubId" :member="member" />
<template #footer>
<button type="button" class="cancel-action" @click="$emit('update:modelValue', false)">
{{ $t('common.close') }}
</button>
</template>
</BaseDialog>
</template>
<script>
import BaseDialog from './BaseDialog.vue';
import OrdersPanel from './OrdersPanel.vue';
export default {
name: 'MemberOrdersDialog',
components: {
BaseDialog,
OrdersPanel
},
props: {
modelValue: {
type: Boolean,
required: true
},
clubId: {
type: [String, Number],
required: true
},
member: {
type: Object,
default: null
}
},
emits: ['update:modelValue', 'close'],
computed: {
dialogTitle() {
if (!this.member) {
return this.$t('orders.title');
}
return this.$t('orders.memberTitle', {
name: [this.member.firstName, this.member.lastName].filter(Boolean).join(' ')
});
}
}
};
</script>

View File

@@ -0,0 +1,503 @@
<template>
<div class="orders-panel">
<div class="orders-toolbar" :class="{ 'orders-toolbar-global': globalMode }">
<div v-if="globalMode" class="orders-filters">
<input
v-model="searchQuery"
type="search"
class="orders-search"
:placeholder="$t('orders.searchPlaceholder')"
>
<select v-model="statusFilter">
<option value="">{{ $t('orders.filterAllStatuses') }}</option>
<option v-for="status in statusOptions" :key="status.value" :value="status.value">
{{ status.label }}
</option>
</select>
<select v-model="clubFilter">
<option value="">{{ $t('orders.filterAllClubs') }}</option>
<option v-for="club in availableClubs" :key="club.id" :value="String(club.id)">
{{ club.name }}
</option>
</select>
</div>
<button type="button" class="btn-secondary" :disabled="loading" @click="loadOrders">
{{ $t('common.refresh') }}
</button>
</div>
<div v-if="!globalMode" class="order-create-card">
<div class="order-create-grid">
<label>
<span>{{ $t('orders.item') }}</span>
<input v-model.trim="newOrder.item" type="text" :placeholder="$t('orders.itemPlaceholder')">
</label>
<label>
<span>{{ $t('orders.cost') }}</span>
<input v-model="newOrder.cost" type="number" min="0" step="0.01">
</label>
<label>
<span>{{ $t('orders.paid') }}</span>
<input v-model="newOrder.paidAmount" type="number" min="0" step="0.01">
</label>
<label>
<span>{{ $t('orders.status') }}</span>
<select v-model="newOrder.status">
<option v-for="status in statusOptions" :key="status.value" :value="status.value">
{{ status.label }}
</option>
</select>
</label>
</div>
<div class="order-create-actions">
<button type="button" class="btn-primary" :disabled="creatingOrder || !newOrder.item" @click="createOrder">
{{ $t('orders.addOrder') }}
</button>
<span class="orders-create-hint">{{ $t('orders.dateAutoHint') }}</span>
</div>
</div>
<div v-if="loading" class="orders-state orders-state-info">{{ $t('orders.loading') }}</div>
<div v-else-if="error" class="orders-state orders-state-error">{{ error }}</div>
<div v-else-if="filteredOrders.length === 0" class="orders-state orders-state-empty">
{{ globalMode ? $t('orders.noOrdersGlobal') : $t('orders.noOrdersMember') }}
</div>
<div v-else class="orders-table-wrap">
<table class="orders-table">
<thead>
<tr>
<th v-if="globalMode">{{ $t('orders.club') }}</th>
<th v-if="globalMode">{{ $t('orders.member') }}</th>
<th>{{ $t('orders.item') }}</th>
<th>{{ $t('orders.status') }}</th>
<th>{{ $t('orders.orderDate') }}</th>
<th>{{ $t('orders.statusDate') }}</th>
<th>{{ $t('orders.cost') }}</th>
<th>{{ $t('orders.paid') }}</th>
<th>{{ $t('orders.open') }}</th>
<th>{{ $t('orders.history') }}</th>
<th>{{ $t('common.save') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="order in filteredOrders" :key="order.id">
<td v-if="globalMode">{{ order.club?.name || '' }}</td>
<td v-if="globalMode">{{ formatMemberName(order.member) }}</td>
<td>
<input v-model.trim="order.draftItem" type="text" class="orders-inline-input">
</td>
<td>
<select v-model="order.draftStatus">
<option v-for="status in statusOptions" :key="status.value" :value="status.value">
{{ status.label }}
</option>
</select>
</td>
<td>{{ formatDateTime(order.orderDate || order.createdAt) }}</td>
<td>{{ formatDateTime(order.statusDate || order.updatedAt) }}</td>
<td>
<input v-model="order.draftCost" type="number" min="0" step="0.01" class="orders-inline-input orders-inline-input-number">
</td>
<td>
<input v-model="order.draftPaidAmount" type="number" min="0" step="0.01" class="orders-inline-input orders-inline-input-number">
</td>
<td>{{ formatCurrency(calculateOpenAmount(order)) }}</td>
<td>
<details class="orders-history-details">
<summary>{{ order.historyEntries?.length || 0 }}</summary>
<div class="orders-history-list">
<div v-for="entry in order.historyEntries || []" :key="entry.id" class="orders-history-entry">
<strong>{{ statusLabel(entry.status) }}</strong>
<span>{{ formatDateTime(entry.changedAt) }}</span>
<span>{{ formatCurrency(entry.cost) }} / {{ formatCurrency(entry.paidAmount) }}</span>
</div>
</div>
</details>
</td>
<td>
<button
type="button"
class="btn-primary btn-small"
:disabled="savingOrderIds.includes(order.id) || !hasOrderChanges(order)"
@click="saveOrder(order)"
>
{{ savingOrderIds.includes(order.id) ? $t('common.loading') : $t('common.save') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import apiClient from '../apiClient.js';
import { getSafeErrorMessage } from '../utils/errorMessages.js';
const STATUS_OPTIONS = [
{ value: 'requested', labelKey: 'orders.statusRequested' },
{ value: 'ordered', labelKey: 'orders.statusOrdered' },
{ value: 'arrived', labelKey: 'orders.statusArrived' },
{ value: 'handed_over', labelKey: 'orders.statusHandedOver' }
];
const normalizeAmount = (value) => {
const parsed = Number.parseFloat(value ?? 0);
if (!Number.isFinite(parsed)) {
return 0;
}
return Math.max(0, Number(parsed.toFixed(2)));
};
export default {
name: 'OrdersPanel',
props: {
clubId: {
type: [String, Number],
default: null
},
member: {
type: Object,
default: null
},
globalMode: {
type: Boolean,
default: false
}
},
data() {
return {
orders: [],
loading: false,
error: '',
creatingOrder: false,
savingOrderIds: [],
searchQuery: '',
statusFilter: '',
clubFilter: '',
newOrder: {
item: '',
status: 'requested',
cost: '',
paidAmount: ''
}
};
},
computed: {
statusOptions() {
return STATUS_OPTIONS.map((status) => ({
...status,
label: this.$t(status.labelKey)
}));
},
availableClubs() {
const map = new Map();
this.orders.forEach((order) => {
if (order.club?.id != null) {
map.set(String(order.club.id), { id: String(order.club.id), name: order.club.name });
}
});
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name, 'de-DE'));
},
filteredOrders() {
return this.orders.filter((order) => {
if (this.statusFilter && order.status !== this.statusFilter) {
return false;
}
if (this.clubFilter && String(order.clubId) !== String(this.clubFilter)) {
return false;
}
if (!this.searchQuery.trim()) {
return true;
}
const search = this.searchQuery.trim().toLowerCase();
const haystack = [
order.item,
order.club?.name,
order.member?.firstName,
order.member?.lastName
].filter(Boolean).join(' ').toLowerCase();
return haystack.includes(search);
});
}
},
watch: {
clubId() {
if (!this.globalMode && this.member?.id) {
this.loadOrders();
}
},
member: {
deep: false,
handler() {
if (!this.globalMode && this.member?.id) {
this.loadOrders();
}
}
}
},
mounted() {
this.loadOrders();
},
methods: {
hydrateOrder(order) {
return {
...order,
cost: normalizeAmount(order.cost),
paidAmount: normalizeAmount(order.paidAmount),
openAmount: normalizeAmount(order.openAmount),
draftItem: order.item || '',
draftStatus: order.status || 'requested',
draftCost: String(normalizeAmount(order.cost)),
draftPaidAmount: String(normalizeAmount(order.paidAmount))
};
},
async loadOrders() {
if (!this.globalMode && (!this.clubId || !this.member?.id)) {
this.orders = [];
return;
}
this.loading = true;
this.error = '';
try {
const url = this.globalMode
? '/member-orders/global'
: `/member-orders/${this.clubId}/${this.member.id}`;
const response = await apiClient.get(url);
const orders = response.data?.orders || [];
this.orders = orders.map((order) => this.hydrateOrder(order));
} catch (error) {
this.error = getSafeErrorMessage(error, this.$t('orders.errorLoading'));
} finally {
this.loading = false;
}
},
async createOrder() {
if (!this.clubId || !this.member?.id || !this.newOrder.item.trim()) {
return;
}
this.creatingOrder = true;
this.error = '';
try {
const response = await apiClient.post(`/member-orders/${this.clubId}/${this.member.id}`, {
item: this.newOrder.item.trim(),
status: this.newOrder.status,
cost: normalizeAmount(this.newOrder.cost),
paidAmount: normalizeAmount(this.newOrder.paidAmount)
});
if (response.data?.order) {
this.orders.unshift(this.hydrateOrder(response.data.order));
this.newOrder = {
item: '',
status: 'requested',
cost: '',
paidAmount: ''
};
}
} catch (error) {
this.error = getSafeErrorMessage(error, this.$t('orders.errorSaving'));
} finally {
this.creatingOrder = false;
}
},
hasOrderChanges(order) {
return order.draftItem !== (order.item || '')
|| order.draftStatus !== order.status
|| normalizeAmount(order.draftCost) !== normalizeAmount(order.cost)
|| normalizeAmount(order.draftPaidAmount) !== normalizeAmount(order.paidAmount);
},
async saveOrder(order) {
this.savingOrderIds.push(order.id);
this.error = '';
try {
const response = await apiClient.patch(`/member-orders/${order.clubId}/${order.memberId}/${order.id}`, {
item: order.draftItem,
status: order.draftStatus,
cost: normalizeAmount(order.draftCost),
paidAmount: normalizeAmount(order.draftPaidAmount)
});
if (response.data?.order) {
const updated = this.hydrateOrder(response.data.order);
this.orders = this.orders.map((entry) => entry.id === order.id ? updated : entry);
}
} catch (error) {
this.error = getSafeErrorMessage(error, this.$t('orders.errorSaving'));
} finally {
this.savingOrderIds = this.savingOrderIds.filter((id) => id !== order.id);
}
},
formatCurrency(value) {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(normalizeAmount(value));
},
formatDateTime(value) {
if (!value) {
return '';
}
return new Intl.DateTimeFormat('de-DE', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(value));
},
formatMemberName(member) {
if (!member) {
return '';
}
return [member.firstName, member.lastName].filter(Boolean).join(' ');
},
calculateOpenAmount(order) {
return Math.max(0, normalizeAmount(order.draftCost) - normalizeAmount(order.draftPaidAmount));
},
statusLabel(status) {
return this.$t(STATUS_OPTIONS.find((entry) => entry.value === status)?.labelKey || 'orders.statusRequested');
}
}
};
</script>
<style scoped>
.orders-panel {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 0;
}
.orders-toolbar,
.orders-filters,
.order-create-actions {
display: flex;
gap: 0.75rem;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.orders-filters {
justify-content: flex-start;
flex: 1;
}
.orders-search {
min-width: 220px;
flex: 1 1 240px;
}
.order-create-card,
.orders-state {
border: 1px solid var(--border-color);
border-radius: 14px;
padding: 1rem;
background: var(--surface-muted);
}
.order-create-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.85rem;
}
.order-create-grid label,
.orders-table td,
.orders-table th {
min-width: 0;
}
.order-create-grid label span {
display: block;
margin-bottom: 0.35rem;
font-size: 0.85rem;
font-weight: 700;
}
.orders-create-hint {
color: var(--text-light);
font-size: 0.9rem;
}
.orders-table-wrap {
overflow: auto;
border: 1px solid var(--border-color);
border-radius: 14px;
background: var(--surface-color);
}
.orders-table {
width: 100%;
min-width: 980px;
border-collapse: collapse;
}
.orders-table th,
.orders-table td {
padding: 0.75rem;
vertical-align: top;
white-space: nowrap;
}
.orders-inline-input {
width: 100%;
min-width: 9rem;
}
.orders-inline-input-number {
min-width: 6.5rem;
}
.orders-history-details summary {
cursor: pointer;
list-style: none;
}
.orders-history-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-top: 0.5rem;
min-width: 16rem;
}
.orders-history-entry {
display: flex;
flex-direction: column;
gap: 0.2rem;
font-size: 0.85rem;
white-space: normal;
}
.orders-state-info {
color: var(--primary-strong);
}
.orders-state-error {
color: #9b2c2c;
background: #fff5f5;
border-color: rgba(155, 44, 44, 0.2);
}
.orders-state-empty {
color: var(--text-light);
}
.btn-small {
padding: 0.45rem 0.7rem;
}
@media (max-width: 768px) {
.orders-search,
.orders-filters select,
.order-create-grid,
.orders-toolbar button,
.order-create-actions button {
width: 100%;
}
.orders-toolbar-global {
align-items: stretch;
}
}
</style>

View File

@@ -28,6 +28,7 @@
"time": "Zyt",
"new": "Neu",
"update": "Aktualisiere",
"refresh": "Neu lade",
"create": "Erstelle",
"remove": "Entferne",
"select": "Uswähle",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Startseite",
"members": "Mitglider",
"orders": "Bestellige",
"diary": "Tagebuech",
"approvals": "Freigabe",
"statistics": "Trainings-Statistik",
@@ -623,6 +625,37 @@
"copyContactSummary": "Kontaktübersicht kopieren",
"copyContactSummarySuccess": "Kontaktübersicht in die Zwischenablage kopiert."
},
"orders": {
"title": "Bestellige",
"memberTitle": "Bestellige: {name}",
"globalTitle": "Bestellige aller Vereine",
"globalSubtitle": "Da chasch alli Bestellige vereinsübergreifend aluege und verwalte.",
"loading": "Bestellige werde glade...",
"errorLoading": "Bestellige hend nöd chöne glade werde.",
"errorSaving": "Bestellig het nöd chöne gspeicheret werde.",
"searchPlaceholder": "Nach Verein, Mitgliid oder Artikel sueche",
"filterAllStatuses": "Alli Status",
"filterAllClubs": "Alli Vereine",
"item": "Was",
"itemPlaceholder": "z. B. Trikot, Hoodie oder Schlägerhülle",
"status": "Status",
"statusRequested": "gwünscht",
"statusOrdered": "bstellt",
"statusArrived": "Artikel achoo",
"statusHandedOver": "Artikel usghändigt",
"cost": "Chöschte",
"paid": "Bezahlt",
"open": "No offe",
"history": "Verlauf",
"orderDate": "Erfasst am",
"statusDate": "Letschti Änderig",
"addOrder": "Bestellig aalege",
"dateAutoHint": "S Datum wird automatisch gsetzt und jedi Änderig mit em Datum protokolliert.",
"noOrdersMember": "Für das Mitgliid git's no kei Bestellige.",
"noOrdersGlobal": "Aktuell git's kei Bestellige.",
"club": "Verein",
"member": "Mitgliid"
},
"diary": {
"title": "Trainingstagebuch",
"date": "Datum",

View File

@@ -26,6 +26,7 @@
"today": "Heute",
"new": "Neu",
"update": "Aktualisieren",
"refresh": "Neu laden",
"create": "Erstellen",
"remove": "Entfernen",
"select": "Auswählen",
@@ -57,6 +58,7 @@
"navigation": {
"home": "Startseite",
"members": "Mitglieder",
"orders": "Bestellungen",
"diary": "Tagebuch",
"approvals": "Freigaben",
"statistics": "Trainings-Statistik",
@@ -294,6 +296,37 @@
"copyContactSummary": "Kontaktübersicht kopieren",
"copyContactSummarySuccess": "Kontaktübersicht in die Zwischenablage kopiert."
},
"orders": {
"title": "Bestellungen",
"memberTitle": "Bestellungen: {name}",
"globalTitle": "Bestellungen aller Vereine",
"globalSubtitle": "Hier werden alle Bestellungen vereinsübergreifend angezeigt und verwaltet.",
"loading": "Lade Bestellungen...",
"errorLoading": "Bestellungen konnten nicht geladen werden.",
"errorSaving": "Bestellung konnte nicht gespeichert werden.",
"searchPlaceholder": "Nach Verein, Mitglied oder Artikel suchen",
"filterAllStatuses": "Alle Status",
"filterAllClubs": "Alle Vereine",
"item": "Was",
"itemPlaceholder": "z. B. Trikot, Hoodie oder Schlägerhülle",
"status": "Status",
"statusRequested": "gewünscht",
"statusOrdered": "bestellt",
"statusArrived": "Artikel angekommen",
"statusHandedOver": "Artikel ausgehändigt",
"cost": "Kosten",
"paid": "Bezahlt",
"open": "Noch offen",
"history": "Verlauf",
"orderDate": "Erfasst am",
"statusDate": "Letzte Änderung",
"addOrder": "Bestellung anlegen",
"dateAutoHint": "Datum wird automatisch gesetzt und jede Änderung mit Datum protokolliert.",
"noOrdersMember": "Für dieses Mitglied gibt es noch keine Bestellungen.",
"noOrdersGlobal": "Es gibt aktuell keine Bestellungen.",
"club": "Verein",
"member": "Mitglied"
},
"diary": {
"title": "Trainingstagebuch",
"date": "Datum",

View File

@@ -28,6 +28,7 @@
"time": "Zeit",
"new": "Neu",
"update": "Aktualisieren",
"refresh": "Neu laden",
"create": "Erstellen",
"remove": "Entfernen",
"select": "Auswählen",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Startseite",
"members": "Mitglieder",
"orders": "Bestellungen",
"diary": "Tagebuch",
"approvals": "Freigaben",
"statistics": "Trainings-Statistik",
@@ -400,6 +402,37 @@
"copyContactSummary": "Kontaktübersicht kopieren",
"copyContactSummarySuccess": "Kontaktübersicht in die Zwischenablage kopiert."
},
"orders": {
"title": "Bestellungen",
"memberTitle": "Bestellungen: {name}",
"globalTitle": "Bestellungen aller Vereine",
"globalSubtitle": "Hier werden alle Bestellungen vereinsübergreifend angezeigt und verwaltet.",
"loading": "Lade Bestellungen...",
"errorLoading": "Bestellungen konnten nicht geladen werden.",
"errorSaving": "Bestellung konnte nicht gespeichert werden.",
"searchPlaceholder": "Nach Verein, Mitglied oder Artikel suchen",
"filterAllStatuses": "Alle Status",
"filterAllClubs": "Alle Vereine",
"item": "Was",
"itemPlaceholder": "z. B. Trikot, Hoodie oder Schlägerhülle",
"status": "Status",
"statusRequested": "gewünscht",
"statusOrdered": "bestellt",
"statusArrived": "Artikel angekommen",
"statusHandedOver": "Artikel ausgehändigt",
"cost": "Kosten",
"paid": "Bezahlt",
"open": "Noch offen",
"history": "Verlauf",
"orderDate": "Erfasst am",
"statusDate": "Letzte Änderung",
"addOrder": "Bestellung anlegen",
"dateAutoHint": "Datum wird automatisch gesetzt und jede Änderung mit Datum protokolliert.",
"noOrdersMember": "Für dieses Mitglied gibt es noch keine Bestellungen.",
"noOrdersGlobal": "Es gibt aktuell keine Bestellungen.",
"club": "Verein",
"member": "Mitglied"
},
"diary": {
"title": "Trainingstagebuch",
"date": "Datum",

View File

@@ -29,6 +29,7 @@
"time": "Time",
"new": "New",
"update": "Update",
"refresh": "Reload",
"create": "Create",
"remove": "Remove",
"select": "Select",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Home",
"members": "Members",
"orders": "Orders",
"diary": "Diary",
"approvals": "Approvals",
"statistics": "Training Statistics",
@@ -623,6 +625,37 @@
"copyContactSummary": "Copy contact summary",
"copyContactSummarySuccess": "Contact summary copied to clipboard."
},
"orders": {
"title": "Orders",
"memberTitle": "Orders: {name}",
"globalTitle": "Orders Across All Clubs",
"globalSubtitle": "All orders across clubs can be viewed and managed here.",
"loading": "Loading orders...",
"errorLoading": "Orders could not be loaded.",
"errorSaving": "Order could not be saved.",
"searchPlaceholder": "Search by club, member or item",
"filterAllStatuses": "All statuses",
"filterAllClubs": "All clubs",
"item": "Item",
"itemPlaceholder": "e.g. shirt, hoodie or bat cover",
"status": "Status",
"statusRequested": "requested",
"statusOrdered": "ordered",
"statusArrived": "item arrived",
"statusHandedOver": "item handed over",
"cost": "Cost",
"paid": "Paid",
"open": "Outstanding",
"history": "History",
"orderDate": "Created on",
"statusDate": "Last change",
"addOrder": "Create order",
"dateAutoHint": "The date is set automatically and every change is logged with a date.",
"noOrdersMember": "There are no orders for this member yet.",
"noOrdersGlobal": "There are currently no orders.",
"club": "Club",
"member": "Member"
},
"diary": {
"title": "Training diary",
"date": "Date",

View File

@@ -29,6 +29,7 @@
"time": "Time",
"new": "New",
"update": "Update",
"refresh": "Reload",
"create": "Create",
"remove": "Remove",
"select": "Select",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Home",
"members": "Members",
"orders": "Orders",
"diary": "Diary",
"approvals": "Approvals",
"statistics": "Training Statistics",
@@ -176,6 +178,37 @@
"confirm": "Confirm",
"cancel": "Cancel"
},
"orders": {
"title": "Orders",
"memberTitle": "Orders: {name}",
"globalTitle": "Orders Across All Clubs",
"globalSubtitle": "All orders across clubs can be viewed and managed here.",
"loading": "Loading orders...",
"errorLoading": "Orders could not be loaded.",
"errorSaving": "Order could not be saved.",
"searchPlaceholder": "Search by club, member or item",
"filterAllStatuses": "All statuses",
"filterAllClubs": "All clubs",
"item": "Item",
"itemPlaceholder": "e.g. shirt, hoodie or bat cover",
"status": "Status",
"statusRequested": "requested",
"statusOrdered": "ordered",
"statusArrived": "item arrived",
"statusHandedOver": "item handed over",
"cost": "Cost",
"paid": "Paid",
"open": "Outstanding",
"history": "History",
"orderDate": "Created on",
"statusDate": "Last change",
"addOrder": "Create order",
"dateAutoHint": "The date is set automatically and every change is logged with a date.",
"noOrdersMember": "There are no orders for this member yet.",
"noOrdersGlobal": "There are currently no orders.",
"club": "Club",
"member": "Member"
},
"diary": {
"title": "Training diary",
"date": "Date",

View File

@@ -29,6 +29,7 @@
"time": "Time",
"new": "New",
"update": "Update",
"refresh": "Reload",
"create": "Create",
"remove": "Remove",
"select": "Select",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Home",
"members": "Members",
"orders": "Orders",
"diary": "Diary",
"approvals": "Approvals",
"statistics": "Training Statistics",
@@ -623,6 +625,37 @@
"copyContactSummary": "Copy contact summary",
"copyContactSummarySuccess": "Contact summary copied to clipboard."
},
"orders": {
"title": "Orders",
"memberTitle": "Orders: {name}",
"globalTitle": "Orders Across All Clubs",
"globalSubtitle": "All orders across clubs can be viewed and managed here.",
"loading": "Loading orders...",
"errorLoading": "Orders could not be loaded.",
"errorSaving": "Order could not be saved.",
"searchPlaceholder": "Search by club, member, or item",
"filterAllStatuses": "All statuses",
"filterAllClubs": "All clubs",
"item": "Item",
"itemPlaceholder": "e.g. jersey, hoodie, or paddle case",
"status": "Status",
"statusRequested": "requested",
"statusOrdered": "ordered",
"statusArrived": "item arrived",
"statusHandedOver": "item handed over",
"cost": "Cost",
"paid": "Paid",
"open": "Outstanding",
"history": "History",
"orderDate": "Created on",
"statusDate": "Last update",
"addOrder": "Create order",
"dateAutoHint": "The date is set automatically and every change is logged with a date.",
"noOrdersMember": "There are no orders for this member yet.",
"noOrdersGlobal": "There are currently no orders.",
"club": "Club",
"member": "Member"
},
"diary": {
"title": "Training diary",
"date": "Date",

View File

@@ -28,6 +28,7 @@
"time": "Hora",
"new": "Nuevo",
"update": "Actualizar",
"refresh": "Recargar",
"create": "Crear",
"remove": "Quitar",
"select": "Seleccionar",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Inicio",
"members": "Miembros",
"orders": "Pedidos",
"diary": "Diario",
"approvals": "Aprobaciones",
"statistics": "Estadísticas de entrenamiento",
@@ -595,6 +597,37 @@
"copyContactSummary": "Copiar resumen de contactos",
"copyContactSummarySuccess": "Resumen de contactos copiado al portapapeles."
},
"orders": {
"title": "Pedidos",
"memberTitle": "Pedidos: {name}",
"globalTitle": "Pedidos de todos los clubes",
"globalSubtitle": "Aquí se pueden ver y gestionar los pedidos de todos los clubes.",
"loading": "Cargando pedidos...",
"errorLoading": "No se pudieron cargar los pedidos.",
"errorSaving": "No se pudo guardar el pedido.",
"searchPlaceholder": "Buscar por club, miembro o artículo",
"filterAllStatuses": "Todos los estados",
"filterAllClubs": "Todos los clubes",
"item": "Artículo",
"itemPlaceholder": "p. ej. camiseta, sudadera o funda de pala",
"status": "Estado",
"statusRequested": "deseado",
"statusOrdered": "pedido",
"statusArrived": "artículo recibido",
"statusHandedOver": "artículo entregado",
"cost": "Coste",
"paid": "Pagado",
"open": "Pendiente",
"history": "Historial",
"orderDate": "Creado el",
"statusDate": "Último cambio",
"addOrder": "Crear pedido",
"dateAutoHint": "La fecha se establece automáticamente y cada cambio se registra con fecha.",
"noOrdersMember": "Todavía no hay pedidos para este miembro.",
"noOrdersGlobal": "Actualmente no hay pedidos.",
"club": "Club",
"member": "Miembro"
},
"diary": {
"title": "Diario de entrenamiento",
"date": "Fecha",

View File

@@ -28,6 +28,7 @@
"time": "Oras",
"new": "Bago",
"update": "I-update",
"refresh": "I-reload",
"create": "Lumikha",
"remove": "Alisin",
"select": "Pumili",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Home",
"members": "Mga miyembro",
"orders": "Mga order",
"diary": "Talaarawan",
"approvals": "Mga pag-apruba",
"statistics": "Istatistika ng pagsasanay",
@@ -595,6 +597,37 @@
"copyContactSummary": "Copy contact summary",
"copyContactSummarySuccess": "Contact summary copied to clipboard."
},
"orders": {
"title": "Mga order",
"memberTitle": "Mga order: {name}",
"globalTitle": "Mga order ng lahat ng club",
"globalSubtitle": "Dito makikita at mapapamahalaan ang lahat ng order mula sa lahat ng club.",
"loading": "Nilo-load ang mga order...",
"errorLoading": "Hindi ma-load ang mga order.",
"errorSaving": "Hindi ma-save ang order.",
"searchPlaceholder": "Maghanap ayon sa club, miyembro, o item",
"filterAllStatuses": "Lahat ng status",
"filterAllClubs": "Lahat ng club",
"item": "Item",
"itemPlaceholder": "hal. jersey, hoodie, o lagayan ng raketa",
"status": "Status",
"statusRequested": "ninanais",
"statusOrdered": "na-order na",
"statusArrived": "dumating na ang item",
"statusHandedOver": "naibigay na ang item",
"cost": "Halaga",
"paid": "Bayad",
"open": "Natitira",
"history": "Kasaysayan",
"orderDate": "Ginawa noong",
"statusDate": "Huling pagbabago",
"addOrder": "Gumawa ng order",
"dateAutoHint": "Awtomatikong itinatakda ang petsa at bawat pagbabago ay sine-save kasama ang petsa.",
"noOrdersMember": "Wala pang order para sa miyembrong ito.",
"noOrdersGlobal": "Wala pang mga order sa ngayon.",
"club": "Club",
"member": "Miyembro"
},
"diary": {
"title": "Talaarawan ng pagsasanay",
"date": "Petsa",

View File

@@ -28,6 +28,7 @@
"time": "Heure",
"new": "Nouveau",
"update": "Mettre à jour",
"refresh": "Recharger",
"create": "Créer",
"remove": "Retirer",
"select": "Sélectionner",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Accueil",
"members": "Membres",
"orders": "Commandes",
"diary": "Journal",
"approvals": "Approbations",
"statistics": "Statistiques d'entraînement",
@@ -595,6 +597,37 @@
"copyContactSummary": "Copier le résumé des contacts",
"copyContactSummarySuccess": "Résumé des contacts copié dans le presse-papiers."
},
"orders": {
"title": "Commandes",
"memberTitle": "Commandes : {name}",
"globalTitle": "Commandes de tous les clubs",
"globalSubtitle": "Toutes les commandes de tous les clubs peuvent être consultées et gérées ici.",
"loading": "Chargement des commandes...",
"errorLoading": "Impossible de charger les commandes.",
"errorSaving": "Impossible d'enregistrer la commande.",
"searchPlaceholder": "Rechercher un club, un membre ou un article",
"filterAllStatuses": "Tous les statuts",
"filterAllClubs": "Tous les clubs",
"item": "Article",
"itemPlaceholder": "p. ex. maillot, hoodie ou housse de raquette",
"status": "Statut",
"statusRequested": "souhaité",
"statusOrdered": "commandé",
"statusArrived": "article arrivé",
"statusHandedOver": "article remis",
"cost": "Coût",
"paid": "Payé",
"open": "Reste à payer",
"history": "Historique",
"orderDate": "Créé le",
"statusDate": "Dernière modification",
"addOrder": "Créer une commande",
"dateAutoHint": "La date est définie automatiquement et chaque modification est enregistrée avec une date.",
"noOrdersMember": "Aucune commande n'existe encore pour ce membre.",
"noOrdersGlobal": "Il n'y a actuellement aucune commande.",
"club": "Club",
"member": "Membre"
},
"diary": {
"title": "Journal d'entraînement",
"date": "Date",

View File

@@ -28,6 +28,7 @@
"time": "Ora",
"new": "Nuovo",
"update": "Aggiorna",
"refresh": "Ricarica",
"create": "Crea",
"remove": "Rimuovi",
"select": "Seleziona",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Home",
"members": "Membri",
"orders": "Ordini",
"diary": "Diario",
"approvals": "Approvazioni",
"statistics": "Statistiche di allenamento",
@@ -595,6 +597,37 @@
"copyContactSummary": "Copia riepilogo contatti",
"copyContactSummarySuccess": "Riepilogo contatti copiato negli appunti."
},
"orders": {
"title": "Ordini",
"memberTitle": "Ordini: {name}",
"globalTitle": "Ordini di tutti i club",
"globalSubtitle": "Qui è possibile visualizzare e gestire tutti gli ordini di tutti i club.",
"loading": "Caricamento ordini...",
"errorLoading": "Impossibile caricare gli ordini.",
"errorSaving": "Impossibile salvare l'ordine.",
"searchPlaceholder": "Cerca per club, membro o articolo",
"filterAllStatuses": "Tutti gli stati",
"filterAllClubs": "Tutti i club",
"item": "Articolo",
"itemPlaceholder": "ad es. maglia, felpa o custodia per racchetta",
"status": "Stato",
"statusRequested": "richiesto",
"statusOrdered": "ordinato",
"statusArrived": "articolo arrivato",
"statusHandedOver": "articolo consegnato",
"cost": "Costo",
"paid": "Pagato",
"open": "Da pagare",
"history": "Cronologia",
"orderDate": "Creato il",
"statusDate": "Ultima modifica",
"addOrder": "Crea ordine",
"dateAutoHint": "La data viene impostata automaticamente e ogni modifica viene registrata con una data.",
"noOrdersMember": "Non ci sono ancora ordini per questo membro.",
"noOrdersGlobal": "Attualmente non ci sono ordini.",
"club": "Club",
"member": "Membro"
},
"diary": {
"title": "Diario di allenamento",
"date": "Data",

View File

@@ -28,6 +28,7 @@
"time": "時刻",
"new": "新規",
"update": "更新",
"refresh": "再読み込み",
"create": "作成",
"remove": "削除",
"select": "選択",
@@ -61,6 +62,7 @@
"navigation": {
"home": "ホーム",
"members": "メンバー",
"orders": "注文",
"diary": "日記",
"approvals": "承認",
"statistics": "トレーニング統計",
@@ -595,6 +597,37 @@
"copyContactSummary": "Copy contact summary",
"copyContactSummarySuccess": "Contact summary copied to clipboard."
},
"orders": {
"title": "注文",
"memberTitle": "注文: {name}",
"globalTitle": "全クラブの注文",
"globalSubtitle": "ここではクラブ横断で全ての注文を表示・管理できます。",
"loading": "注文を読み込み中...",
"errorLoading": "注文を読み込めませんでした。",
"errorSaving": "注文を保存できませんでした。",
"searchPlaceholder": "クラブ、メンバー、または商品で検索",
"filterAllStatuses": "すべての状態",
"filterAllClubs": "すべてのクラブ",
"item": "商品",
"itemPlaceholder": "例: シャツ、パーカー、ラケットケース",
"status": "状態",
"statusRequested": "希望",
"statusOrdered": "注文済み",
"statusArrived": "商品到着",
"statusHandedOver": "商品引き渡し済み",
"cost": "費用",
"paid": "支払済み",
"open": "未払い",
"history": "履歴",
"orderDate": "登録日",
"statusDate": "最終変更",
"addOrder": "注文を追加",
"dateAutoHint": "日付は自動設定され、すべての変更が日付付きで記録されます。",
"noOrdersMember": "このメンバーにはまだ注文がありません。",
"noOrdersGlobal": "現在注文はありません。",
"club": "クラブ",
"member": "メンバー"
},
"diary": {
"title": "練習日誌",
"date": "日付",

View File

@@ -28,6 +28,7 @@
"time": "Czas",
"new": "Nowy",
"update": "Aktualizuj",
"refresh": "Odśwież",
"create": "Utwórz",
"remove": "Usuń",
"select": "Wybierz",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Strona główna",
"members": "Członkowie",
"orders": "Zamówienia",
"diary": "Dziennik",
"approvals": "Zatwierdzenia",
"statistics": "Statystyki treningowe",
@@ -595,6 +597,37 @@
"copyContactSummary": "Kopiuj podsumowanie kontaktów",
"copyContactSummarySuccess": "Podsumowanie kontaktów skopiowano do schowka."
},
"orders": {
"title": "Zamówienia",
"memberTitle": "Zamówienia: {name}",
"globalTitle": "Zamówienia ze wszystkich klubów",
"globalSubtitle": "Tutaj można przeglądać i zarządzać zamówieniami ze wszystkich klubów.",
"loading": "Ładowanie zamówień...",
"errorLoading": "Nie udało się załadować zamówień.",
"errorSaving": "Nie udało się zapisać zamówienia.",
"searchPlaceholder": "Szukaj po klubie, członku lub artykule",
"filterAllStatuses": "Wszystkie statusy",
"filterAllClubs": "Wszystkie kluby",
"item": "Artykuł",
"itemPlaceholder": "np. koszulka, bluza lub pokrowiec na rakietkę",
"status": "Status",
"statusRequested": "pożądane",
"statusOrdered": "zamówione",
"statusArrived": "artykuł dotarł",
"statusHandedOver": "artykuł wydany",
"cost": "Koszt",
"paid": "Zapłacono",
"open": "Pozostało",
"history": "Historia",
"orderDate": "Utworzono",
"statusDate": "Ostatnia zmiana",
"addOrder": "Dodaj zamówienie",
"dateAutoHint": "Data jest ustawiana automatycznie, a każda zmiana jest zapisywana z datą.",
"noOrdersMember": "Ten członek nie ma jeszcze żadnych zamówień.",
"noOrdersGlobal": "Obecnie nie ma żadnych zamówień.",
"club": "Klub",
"member": "Członek"
},
"diary": {
"title": "Dziennik treningowy",
"date": "Data",

View File

@@ -28,6 +28,7 @@
"time": "เวลา",
"new": "ใหม่",
"update": "อัปเดต",
"refresh": "โหลดใหม่",
"create": "สร้าง",
"remove": "เอาออก",
"select": "เลือก",
@@ -61,6 +62,7 @@
"navigation": {
"home": "หน้าแรก",
"members": "สมาชิก",
"orders": "คำสั่งซื้อ",
"diary": "ไดอารี่",
"approvals": "การอนุมัติ",
"statistics": "สถิติการฝึกซ้อม",
@@ -595,6 +597,37 @@
"copyContactSummary": "Copy contact summary",
"copyContactSummarySuccess": "Contact summary copied to clipboard."
},
"orders": {
"title": "คำสั่งซื้อ",
"memberTitle": "คำสั่งซื้อ: {name}",
"globalTitle": "คำสั่งซื้อของทุกสโมสร",
"globalSubtitle": "สามารถดูและจัดการคำสั่งซื้อทั้งหมดข้ามทุกสโมสรได้ที่นี่",
"loading": "กำลังโหลดคำสั่งซื้อ...",
"errorLoading": "ไม่สามารถโหลดคำสั่งซื้อได้",
"errorSaving": "ไม่สามารถบันทึกคำสั่งซื้อได้",
"searchPlaceholder": "ค้นหาตามสโมสร สมาชิก หรือสินค้า",
"filterAllStatuses": "ทุกสถานะ",
"filterAllClubs": "ทุกสโมสร",
"item": "สินค้า",
"itemPlaceholder": "เช่น เสื้อแข่ง ฮู้ดดี้ หรือซองไม้",
"status": "สถานะ",
"statusRequested": "ต้องการ",
"statusOrdered": "สั่งแล้ว",
"statusArrived": "สินค้ามาถึงแล้ว",
"statusHandedOver": "ส่งมอบสินค้าแล้ว",
"cost": "ค่าใช้จ่าย",
"paid": "ชำระแล้ว",
"open": "ค้างชำระ",
"history": "ประวัติ",
"orderDate": "วันที่สร้าง",
"statusDate": "การเปลี่ยนแปลงล่าสุด",
"addOrder": "สร้างคำสั่งซื้อ",
"dateAutoHint": "ระบบจะกำหนดวันที่ให้อัตโนมัติ และทุกการเปลี่ยนแปลงจะถูกบันทึกพร้อมวันที่",
"noOrdersMember": "ยังไม่มีคำสั่งซื้อสำหรับสมาชิกคนนี้",
"noOrdersGlobal": "ขณะนี้ยังไม่มีคำสั่งซื้อ",
"club": "สโมสร",
"member": "สมาชิก"
},
"diary": {
"title": "สมุดบันทึกการฝึกซ้อม",
"date": "วันที่",

View File

@@ -28,6 +28,7 @@
"time": "Oras",
"new": "Bago",
"update": "I-update",
"refresh": "I-reload",
"create": "Lumikha",
"remove": "Alisin",
"select": "Pumili",
@@ -61,6 +62,7 @@
"navigation": {
"home": "Home",
"members": "Mga miyembro",
"orders": "Mga order",
"diary": "Talaarawan",
"approvals": "Mga pag-apruba",
"statistics": "Mga istatistika ng pagsasanay",
@@ -595,6 +597,37 @@
"copyContactSummary": "Copy contact summary",
"copyContactSummarySuccess": "Contact summary copied to clipboard."
},
"orders": {
"title": "Mga order",
"memberTitle": "Mga order: {name}",
"globalTitle": "Mga order ng lahat ng club",
"globalSubtitle": "Dito makikita at mapapamahalaan ang lahat ng order mula sa ibat ibang club.",
"loading": "Nilo-load ang mga order...",
"errorLoading": "Hindi ma-load ang mga order.",
"errorSaving": "Hindi ma-save ang order.",
"searchPlaceholder": "Maghanap ayon sa club, miyembro, o item",
"filterAllStatuses": "Lahat ng status",
"filterAllClubs": "Lahat ng club",
"item": "Item",
"itemPlaceholder": "hal. jersey, hoodie, o lagayan ng raketa",
"status": "Status",
"statusRequested": "hinihiling",
"statusOrdered": "na-order na",
"statusArrived": "dumating na ang item",
"statusHandedOver": "naibigay na ang item",
"cost": "Halaga",
"paid": "Nabayaran",
"open": "Natitira",
"history": "Kasaysayan",
"orderDate": "Ginawa noong",
"statusDate": "Huling pagbabago",
"addOrder": "Gumawa ng order",
"dateAutoHint": "Awtomatikong itinatakda ang petsa at bawat pagbabago ay sine-save kasama ang petsa.",
"noOrdersMember": "Wala pang order para sa miyembrong ito.",
"noOrdersGlobal": "Wala pang mga order sa ngayon.",
"club": "Club",
"member": "Miyembro"
},
"diary": {
"title": "Talaarawan ng pagsasanay",
"date": "Petsa",

View File

@@ -28,6 +28,7 @@
"time": "时间",
"new": "新建",
"update": "更新",
"refresh": "重新加载",
"create": "创建",
"remove": "移除",
"select": "选择",
@@ -61,6 +62,7 @@
"navigation": {
"home": "首页",
"members": "成员",
"orders": "订单",
"diary": "日记",
"approvals": "审批",
"statistics": "训练统计",
@@ -595,6 +597,37 @@
"copyContactSummary": "Copy contact summary",
"copyContactSummarySuccess": "Contact summary copied to clipboard."
},
"orders": {
"title": "订单",
"memberTitle": "订单:{name}",
"globalTitle": "所有俱乐部的订单",
"globalSubtitle": "可在此跨俱乐部查看和管理所有订单。",
"loading": "正在加载订单...",
"errorLoading": "无法加载订单。",
"errorSaving": "无法保存订单。",
"searchPlaceholder": "按俱乐部、成员或商品搜索",
"filterAllStatuses": "所有状态",
"filterAllClubs": "所有俱乐部",
"item": "商品",
"itemPlaceholder": "例如:球衣、卫衣或球拍套",
"status": "状态",
"statusRequested": "希望购买",
"statusOrdered": "已订购",
"statusArrived": "商品已到",
"statusHandedOver": "商品已发放",
"cost": "费用",
"paid": "已支付",
"open": "未结清",
"history": "历史",
"orderDate": "创建时间",
"statusDate": "最后变更",
"addOrder": "新建订单",
"dateAutoHint": "日期会自动设置,并且每次变更都会连同日期一起记录。",
"noOrdersMember": "该成员目前还没有订单。",
"noOrdersGlobal": "当前没有订单。",
"club": "俱乐部",
"member": "成员"
},
"diary": {
"title": "训练日记",
"date": "日期",

View File

@@ -27,6 +27,7 @@ const LogsView = () => import('./views/LogsView.vue');
const ClickTtView = () => import('./views/ClickTtView.vue');
const MemberTransferSettingsView = () => import('./views/MemberTransferSettingsView.vue');
const PersonalSettings = () => import('./views/PersonalSettings.vue');
const OrdersView = () => import('./views/OrdersView.vue');
const Impressum = () => import('./views/Impressum.vue');
const Datenschutz = () => import('./views/Datenschutz.vue');
@@ -56,6 +57,7 @@ const routes = [
{ path: '/clicktt', name: 'clicktt', component: ClickTtView },
{ path: '/member-transfer-settings', name: 'member-transfer-settings', component: MemberTransferSettingsView },
{ path: '/personal-settings', name: 'personal-settings', component: PersonalSettings },
{ path: '/orders', name: 'orders', component: OrdersView },
{ path: '/impressum', name: 'impressum', component: Impressum, meta: { public: true } },
{ path: '/datenschutz', name: 'datenschutz', component: Datenschutz, meta: { public: true } },
];

View File

@@ -566,6 +566,7 @@
@open-notes="openNotesModal"
@show-pic="showPic"
@mark-form="markFormHandedOver"
@open-orders="openOrdersDialog"
@open-tags="openTagInfos"
@open-quick-add="openQuickAddDialog"
@open-gallery="openGalleryDialog"
@@ -671,6 +672,14 @@
:should-show-member="shouldShowGalleryMember"
@member-click="handleGalleryMemberClick"
/>
<MemberOrdersDialog
v-if="showMemberOrdersDialog && selectedMemberForOrders"
v-model="showMemberOrdersDialog"
:member="selectedMemberForOrders"
:club-id="currentClub"
@close="closeOrdersDialog"
/>
</div>
<!-- Info Dialog -->
@@ -712,6 +721,7 @@ import MemberActivityStatsDialog from '../components/MemberActivityStatsDialog.v
import AccidentFormDialog from '../components/AccidentFormDialog.vue';
import QuickAddMemberDialog from '../components/QuickAddMemberDialog.vue';
import MemberGalleryDialog from '../components/MemberGalleryDialog.vue';
import MemberOrdersDialog from '../components/MemberOrdersDialog.vue';
import DiaryParticipantsPanel from '../components/DiaryParticipantsPanel.vue';
import DiaryActivitiesPanel from '../components/DiaryActivitiesPanel.vue';
import DiaryOverviewPanels from '../components/diary/DiaryOverviewPanels.vue';
@@ -776,6 +786,7 @@ export default {
AccidentFormDialog,
QuickAddMemberDialog,
MemberGalleryDialog,
MemberOrdersDialog,
DiaryParticipantsPanel,
DiaryActivitiesPanel,
DiaryOverviewPanels
@@ -818,8 +829,10 @@ export default {
notes: [],
newNoteContent: '',
noteMember: null,
selectedMemberForOrders: null,
selectedMember: null,
showNotesModal: false,
showMemberOrdersDialog: false,
selectedActivityTags: [],
selectedMemberTags: [],
selectedMemberDayTags: [],
@@ -1797,6 +1810,14 @@ export default {
this.loadMemberNotesAndTags(this.date.id, member.id);
this.showNotesModal = true;
},
openOrdersDialog(member) {
this.selectedMemberForOrders = member;
this.showMemberOrdersDialog = true;
},
closeOrdersDialog() {
this.showMemberOrdersDialog = false;
this.selectedMemberForOrders = null;
},
async loadMemberNotesAndTags(diaryDateId, memberId) {
this.doMemberTagUpdates = false;

View File

@@ -74,6 +74,7 @@
</div>
<div class="member-preview-actions">
<button type="button" class="btn-primary" @click="editMember(selectedMemberPreview)">{{ $t('members.editMember') }}</button>
<button type="button" @click="openOrdersDialog(selectedMemberPreview)">{{ $t('orders.title') }}</button>
<button type="button" @click="openImageModal(selectedMemberPreview)">{{ $t('members.memberImages') }}</button>
<button type="button" @click="openNotesModal(selectedMemberPreview)">{{ $t('members.notes') }}</button>
<button type="button" @click="openActivitiesModal(selectedMemberPreview)">{{ $t('members.exercises') }}</button>
@@ -440,6 +441,7 @@
{{ clickTtPendingMemberIds.includes(member.id) ? '⏳' : '🏓' }}
</button>
<button type="button" class="member-icon-button member-icon-button-edit" :title="$t('members.editMember')" @click.stop="editMember(member)">🪶</button>
<button type="button" class="member-icon-button" :title="$t('orders.title')" @click.stop="openOrdersDialog(member)">📦</button>
<button type="button" class="member-icon-button" :title="$t('members.notes')" @click.stop="openNotesModal(member)">📝</button>
<button type="button" class="member-icon-button" :title="$t('members.exercises')" @click.stop="openActivitiesModal(member)">🏃</button>
<button
@@ -536,6 +538,14 @@
:club-id="currentClub"
@close="closeTtrHistoryDialog"
/>
<MemberOrdersDialog
v-if="showMemberOrdersDialog && selectedMemberForOrders"
v-model="showMemberOrdersDialog"
:member="selectedMemberForOrders"
:club-id="currentClub"
@close="closeOrdersDialog"
/>
</div>
</template>
@@ -553,6 +563,7 @@ import MemberNotesDialog from '../components/MemberNotesDialog.vue';
import MemberActivitiesDialog from '../components/MemberActivitiesDialog.vue';
import MemberTransferDialog from '../components/MemberTransferDialog.vue';
import MemberTtrHistoryDialog from '../components/MemberTtrHistoryDialog.vue';
import MemberOrdersDialog from '../components/MemberOrdersDialog.vue';
import MembersOverviewSection from '../components/members/MembersOverviewSection.vue';
import { debounce } from '../utils/debounce.js';
import { buildInfoConfig, buildConfirmConfig, safeErrorMessage, sanitizeText } from '../utils/dialogUtils.js';
@@ -567,6 +578,7 @@ export default {
MemberActivitiesDialog,
MemberTransferDialog,
MemberTtrHistoryDialog,
MemberOrdersDialog,
MembersOverviewSection
},
computed: {
@@ -903,8 +915,10 @@ export default {
showMemberInfo: false,
showActivitiesModal: false,
showMemberTtrHistoryDialog: false,
showMemberOrdersDialog: false,
selectedMemberForActivities: null,
selectedMemberForTtrHistory: null,
selectedMemberForOrders: null,
memberTrainingGroups: [],
trainingGroups: [],
selectedGroupToAdd: '',
@@ -2279,6 +2293,17 @@ export default {
this.showMemberTtrHistoryDialog = false;
this.selectedMemberForTtrHistory = null;
},
openOrdersDialog(member) {
if (!member) {
return;
}
this.selectedMemberForOrders = member;
this.showMemberOrdersDialog = true;
},
closeOrdersDialog() {
this.showMemberOrdersDialog = false;
this.selectedMemberForOrders = null;
},
async updateRatingsFromMyTischtennis() {
this.isUpdatingRatings = true;
try {

View File

@@ -0,0 +1,35 @@
<template>
<div class="orders-view">
<div class="page-header">
<div>
<h2>{{ $t('orders.globalTitle') }}</h2>
<p class="orders-view-subtitle">{{ $t('orders.globalSubtitle') }}</p>
</div>
</div>
<OrdersPanel global-mode />
</div>
</template>
<script>
import OrdersPanel from '../components/OrdersPanel.vue';
export default {
name: 'OrdersView',
components: {
OrdersPanel
}
};
</script>
<style scoped>
.orders-view {
display: flex;
flex-direction: column;
gap: 1rem;
}
.orders-view-subtitle {
margin: 0.35rem 0 0;
color: var(--text-light);
}
</style>