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:
parent
ebab892e93
commit
dcc466439e
2 changed files with 156 additions and 3 deletions
|
|
@ -24,3 +24,13 @@ export function updateOrderStatus(
|
||||||
body: JSON.stringify({ status }),
|
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 }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import { fetchAllOrders, updateOrderStatus, type AdminOrder } from '@/api/admin'
|
import {
|
||||||
|
fetchAllOrders,
|
||||||
|
updateOrderStatus,
|
||||||
|
updateTracking,
|
||||||
|
type AdminOrder,
|
||||||
|
} from '@/api/admin'
|
||||||
|
|
||||||
const orders = ref<AdminOrder[]>([])
|
const orders = ref<AdminOrder[]>([])
|
||||||
const expandedOrderId = ref<string | null>(null)
|
const expandedOrderId = ref<string | null>(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const statusError = ref('')
|
const statusError = ref('')
|
||||||
|
const trackingError = ref('')
|
||||||
|
const trackingInputValues = reactive<Record<string, string>>({})
|
||||||
|
|
||||||
const statusLabels: Record<string, string> = {
|
const statusLabels: Record<string, string> = {
|
||||||
pending_payment: 'Väntar på betalning',
|
pending_payment: 'Väntar på betalning',
|
||||||
|
|
@ -44,7 +51,15 @@ function formatDate(iso: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleExpand(orderId: 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) {
|
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 () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
orders.value = await fetchAllOrders()
|
orders.value = await fetchAllOrders()
|
||||||
|
|
@ -153,6 +185,51 @@ onMounted(async () => {
|
||||||
{{ order.letterText }}
|
{{ order.letterText }}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -321,4 +398,70 @@ onMounted(async () => {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
white-space: pre-wrap;
|
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>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue