feat: add tracking input, save button, and PostNord link to admin dashboard

- api/admin.ts: updateTracking(orderId, trackingId) calls PATCH
  /api/admin/orders/{id} with JSON { trackingId }
- AdminPage.vue expanded row: add "Spårnings-ID" section below
  Brevtext with text input, save button, and PostNord link
- trackingInputValues reactive map tracks per-order input state
- toggleExpand initialises trackingInputValues[orderId] from
  order.trackingId on first expand
- handleTrackingSave: PATCH API call with optimistic local update,
  reverts on error, shows red inline error
- PostNord link (<a target="_blank">): https://www.postnord.se/
  verktyg/spara/?id={trackingId}, only visible when trackingId
  is non-null
- trackingError ref for inline error state
- CSS: tracking section styling, input focus ring, blue save button
This commit is contained in:
Joakim Mörling 2026-05-15 19:58:46 +02:00
parent ebab892e93
commit dcc466439e
2 changed files with 156 additions and 3 deletions

View file

@ -24,3 +24,13 @@ export function updateOrderStatus(
body: JSON.stringify({ status }),
})
}
export function updateTracking(
orderId: string,
trackingId: string | null,
): Promise<AdminOrder> {
return request<AdminOrder>(`/admin/orders/${orderId}`, {
method: 'PATCH',
body: JSON.stringify({ trackingId }),
})
}

View file

@ -1,12 +1,19 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { fetchAllOrders, updateOrderStatus, type AdminOrder } from '@/api/admin'
import { ref, onMounted, reactive } from 'vue'
import {
fetchAllOrders,
updateOrderStatus,
updateTracking,
type AdminOrder,
} from '@/api/admin'
const orders = ref<AdminOrder[]>([])
const expandedOrderId = ref<string | null>(null)
const loading = ref(true)
const error = ref('')
const statusError = ref('')
const trackingError = ref('')
const trackingInputValues = reactive<Record<string, string>>({})
const statusLabels: Record<string, string> = {
pending_payment: 'Väntar på betalning',
@ -44,7 +51,15 @@ function formatDate(iso: string): string {
}
function toggleExpand(orderId: string) {
expandedOrderId.value = expandedOrderId.value === orderId ? null : orderId
if (expandedOrderId.value === orderId) {
expandedOrderId.value = null
} else {
expandedOrderId.value = orderId
const order = orders.value.find((o) => o.id === orderId)
if (order && !(orderId in trackingInputValues)) {
trackingInputValues[orderId] = order.trackingId ?? ''
}
}
}
async function handleStatusChange(orderId: string, newStatus: string) {
@ -63,6 +78,23 @@ async function handleStatusChange(orderId: string, newStatus: string) {
}
}
async function handleTrackingSave(orderId: string) {
const newTrackingId = trackingInputValues[orderId]?.trim() || null
const order = orders.value.find((o) => o.id === orderId)
if (!order) return
const previousTrackingId = order.trackingId
order.trackingId = newTrackingId
trackingError.value = ''
try {
await updateTracking(orderId, newTrackingId)
} catch {
order.trackingId = previousTrackingId
trackingError.value = 'Kunde inte spara spårnings-ID. Försök igen.'
}
}
onMounted(async () => {
try {
orders.value = await fetchAllOrders()
@ -153,6 +185,51 @@ onMounted(async () => {
{{ order.letterText }}
</div>
</div>
<div class="admin-dashboard__tracking">
<div class="admin-dashboard__tracking-header">
<span class="admin-dashboard__tracking-label"
>Spårnings-ID</span
>
<a
v-if="order.trackingId"
class="admin-dashboard__tracking-link"
:href="`https://www.postnord.se/verktyg/spara/?id=${order.trackingId}`"
target="_blank"
rel="noopener noreferrer"
@click.stop
>
Spåra hos PostNord
</a>
</div>
<p v-if="trackingError" class="admin-dashboard__status-error">
{{ trackingError }}
</p>
<div class="admin-dashboard__tracking-input-row">
<input
class="admin-dashboard__tracking-input"
type="text"
:value="
trackingInputValues[order.id] ?? order.trackingId ?? ''
"
placeholder="PN..."
@input="
trackingInputValues[order.id] = (
$event.target as HTMLInputElement
).value
"
@click.stop
/>
<button
class="admin-dashboard__tracking-save"
@click.stop="handleTrackingSave(order.id)"
>
Spara spårning
</button>
</div>
</div>
</td>
</tr>
</template>
@ -321,4 +398,70 @@ onMounted(async () => {
line-height: 1.6;
white-space: pre-wrap;
}
.admin-dashboard__tracking {
padding: 1rem 1.25rem;
border-top: 1px solid #e2e8f0;
}
.admin-dashboard__tracking-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.admin-dashboard__tracking-label {
font-size: 0.75rem;
font-weight: 600;
color: #a0aec0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.admin-dashboard__tracking-link {
font-size: 0.8125rem;
color: #4299e1;
text-decoration: none;
}
.admin-dashboard__tracking-link:hover {
text-decoration: underline;
}
.admin-dashboard__tracking-input-row {
display: flex;
gap: 0.5rem;
}
.admin-dashboard__tracking-input {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
font-size: 0.8125rem;
color: #4a5568;
outline: none;
}
.admin-dashboard__tracking-input:focus {
border-color: #4299e1;
box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.2);
}
.admin-dashboard__tracking-save {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.375rem;
background: #4299e1;
color: #fff;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
.admin-dashboard__tracking-save:hover {
background: #3182ce;
}
</style>