feat: initial commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Gold Bank — Admin Panel</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "admin-panel",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"lint": "eslint \"src/**/*.{ts,vue}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@gold-bank/shared-types": "workspace:*",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.4.0",
|
||||
"pinia": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "^5.4.0",
|
||||
"vue-tsc": "^2.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<h2>Gold Bank — Admin</h2>
|
||||
<nav>
|
||||
<router-link to="/">Dashboard</router-link>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<router-view />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
main {
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
export default _default;
|
||||
@@ -0,0 +1,39 @@
|
||||
/// <reference types="../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
let __VLS_directives;
|
||||
// CSS variable injection
|
||||
// CSS variable injection end
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.header, __VLS_intrinsicElements.header)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.nav, __VLS_intrinsicElements.nav)({});
|
||||
const __VLS_0 = {}.RouterLink;
|
||||
/** @type {[typeof __VLS_components.RouterLink, typeof __VLS_components.routerLink, typeof __VLS_components.RouterLink, typeof __VLS_components.routerLink, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_1 = __VLS_asFunctionalComponent(__VLS_0, new __VLS_0({
|
||||
to: "/",
|
||||
}));
|
||||
const __VLS_2 = __VLS_1({
|
||||
to: "/",
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
|
||||
__VLS_3.slots.default;
|
||||
var __VLS_3;
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.main, __VLS_intrinsicElements.main)({});
|
||||
const __VLS_4 = {}.RouterView;
|
||||
/** @type {[typeof __VLS_components.RouterView, typeof __VLS_components.routerView, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_5 = __VLS_asFunctionalComponent(__VLS_4, new __VLS_4({}));
|
||||
const __VLS_6 = __VLS_5({}, ...__VLS_functionalComponentArgsRest(__VLS_5));
|
||||
var __VLS_dollars;
|
||||
const __VLS_self = (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
export default (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
; /* PartiallyEnd: #4569/main.vue */
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
import type { CreateOrderDto, CustomerBalanceDto } from '@gold-bank/shared-types';
|
||||
export interface CustomerSummary {
|
||||
id: string;
|
||||
fullName: string;
|
||||
mobile: string;
|
||||
goldDebtGrams: string;
|
||||
fiatDebtRial: string;
|
||||
goldCreditLimitGrams: string;
|
||||
}
|
||||
export interface ProductVariantSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
rashWeightGrams: string;
|
||||
defaultWagePercent: string;
|
||||
}
|
||||
export interface ProductSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
variants: ProductVariantSummary[];
|
||||
}
|
||||
export interface OrderDetail {
|
||||
id: string;
|
||||
orderNumber: string;
|
||||
status: string;
|
||||
totalGoldWeightGrams: string;
|
||||
wageWeightGrams: string;
|
||||
flatRialCharge: string;
|
||||
isSuspendedOverCreditLimit: boolean;
|
||||
deliveredAt: string | null;
|
||||
items: Array<{
|
||||
id: string;
|
||||
quantity: number;
|
||||
computedWeightGrams: string;
|
||||
}>;
|
||||
statusEvents: Array<{
|
||||
toStatus: string;
|
||||
note: string | null;
|
||||
createdAt: string;
|
||||
}>;
|
||||
}
|
||||
export declare const api: {
|
||||
listCustomers: () => Promise<CustomerSummary[]>;
|
||||
getCustomerBalance: (id: string) => Promise<CustomerBalanceDto>;
|
||||
listProducts: () => Promise<ProductSummary[]>;
|
||||
createOrder: (dto: CreateOrderDto) => Promise<OrderDetail>;
|
||||
getOrder: (id: string) => Promise<OrderDetail>;
|
||||
updateOrderStatus: (id: string, status: string, note?: string) => Promise<OrderDetail>;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
const BASE = '/api';
|
||||
async function request(path, options) {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`API ${path} failed (${res.status}): ${body}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
export const api = {
|
||||
listCustomers: () => request('/customers'),
|
||||
getCustomerBalance: (id) => request(`/accounting/customers/${id}/balance`),
|
||||
listProducts: () => request('/catalog/products'),
|
||||
createOrder: (dto) => request('/orders', { method: 'POST', body: JSON.stringify(dto) }),
|
||||
getOrder: (id) => request(`/orders/${id}`),
|
||||
updateOrderStatus: (id, status, note) => request(`/orders/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status, note }) }),
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { CreateOrderDto, CustomerBalanceDto } from '@gold-bank/shared-types';
|
||||
|
||||
const BASE = '/api';
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`API ${path} failed (${res.status}): ${body}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface CustomerSummary {
|
||||
id: string;
|
||||
fullName: string;
|
||||
mobile: string;
|
||||
goldDebtGrams: string;
|
||||
fiatDebtRial: string;
|
||||
goldCreditLimitGrams: string;
|
||||
}
|
||||
|
||||
export interface ProductVariantSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
rashWeightGrams: string;
|
||||
defaultWagePercent: string;
|
||||
}
|
||||
|
||||
export interface ProductSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
variants: ProductVariantSummary[];
|
||||
}
|
||||
|
||||
export interface OrderDetail {
|
||||
id: string;
|
||||
orderNumber: string;
|
||||
status: string;
|
||||
totalGoldWeightGrams: string;
|
||||
wageWeightGrams: string;
|
||||
flatRialCharge: string;
|
||||
isSuspendedOverCreditLimit: boolean;
|
||||
deliveredAt: string | null;
|
||||
items: Array<{ id: string; quantity: number; computedWeightGrams: string }>;
|
||||
statusEvents: Array<{ toStatus: string; note: string | null; createdAt: string }>;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
listCustomers: () => request<CustomerSummary[]>('/customers'),
|
||||
getCustomerBalance: (id: string) => request<CustomerBalanceDto>(`/accounting/customers/${id}/balance`),
|
||||
listProducts: () => request<ProductSummary[]>('/catalog/products'),
|
||||
createOrder: (dto: CreateOrderDto) =>
|
||||
request<OrderDetail>('/orders', { method: 'POST', body: JSON.stringify(dto) }),
|
||||
getOrder: (id: string) => request<OrderDetail>(`/orders/${id}`),
|
||||
updateOrderStatus: (id: string, status: string, note?: string) =>
|
||||
request<OrderDetail>(`/orders/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status, note }) }),
|
||||
};
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
|
||||
export default component;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import App from './App.vue';
|
||||
import { router } from './router';
|
||||
createApp(App).use(createPinia()).use(router).mount('#app');
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import App from './App.vue';
|
||||
import { router } from './router';
|
||||
|
||||
createApp(App).use(createPinia()).use(router).mount('#app');
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare const router: import("vue-router").Router;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import DashboardView from '../views/DashboardView.vue';
|
||||
import OrderNewView from '../views/OrderNewView.vue';
|
||||
import OrderDetailView from '../views/OrderDetailView.vue';
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'dashboard', component: DashboardView },
|
||||
{ path: '/orders/new', name: 'order-new', component: OrderNewView },
|
||||
{ path: '/orders/:id', name: 'order-detail', component: OrderDetailView },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import DashboardView from '../views/DashboardView.vue';
|
||||
import OrderNewView from '../views/OrderNewView.vue';
|
||||
import OrderDetailView from '../views/OrderDetailView.vue';
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'dashboard', component: DashboardView },
|
||||
{ path: '/orders/new', name: 'order-new', component: OrderNewView },
|
||||
{ path: '/orders/:id', name: 'order-detail', component: OrderDetailView },
|
||||
],
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Placeholder for the logged-in admin's identity. No auth module exists yet
|
||||
* (see migration notes) — this just tracks UI selection state until then.
|
||||
*/
|
||||
export declare const useSessionStore: import("pinia").StoreDefinition<"session", {
|
||||
selectedCustomerId: string | null;
|
||||
}, {}, {
|
||||
selectCustomer(customerId: string): void;
|
||||
}>;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineStore } from 'pinia';
|
||||
/**
|
||||
* Placeholder for the logged-in admin's identity. No auth module exists yet
|
||||
* (see migration notes) — this just tracks UI selection state until then.
|
||||
*/
|
||||
export const useSessionStore = defineStore('session', {
|
||||
state: () => ({
|
||||
selectedCustomerId: null,
|
||||
}),
|
||||
actions: {
|
||||
selectCustomer(customerId) {
|
||||
this.selectedCustomerId = customerId;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/**
|
||||
* Placeholder for the logged-in admin's identity. No auth module exists yet
|
||||
* (see migration notes) — this just tracks UI selection state until then.
|
||||
*/
|
||||
export const useSessionStore = defineStore('session', {
|
||||
state: () => ({
|
||||
selectedCustomerId: null as string | null,
|
||||
}),
|
||||
actions: {
|
||||
selectCustomer(customerId: string) {
|
||||
this.selectedCustomerId = customerId;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { api, type CustomerSummary } from '../api/client';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
|
||||
const customers = ref<CustomerSummary[]>([]);
|
||||
const loading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
const router = useRouter();
|
||||
const session = useSessionStore();
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
customers.value = await api.listCustomers();
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
function goToNewOrder(customerId: string) {
|
||||
session.selectCustomer(customerId);
|
||||
router.push({ name: 'order-new' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<h1>Customers</h1>
|
||||
<p v-if="loading">Loading…</p>
|
||||
<p v-else-if="error" class="error">{{ error }}</p>
|
||||
<table v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Mobile</th>
|
||||
<th>Gold debt (g)</th>
|
||||
<th>Rial debt</th>
|
||||
<th>Credit limit (g)</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="c in customers" :key="c.id">
|
||||
<td>{{ c.fullName }}</td>
|
||||
<td>{{ c.mobile }}</td>
|
||||
<td>{{ c.goldDebtGrams }}</td>
|
||||
<td>{{ c.fiatDebtRial }}</td>
|
||||
<td>{{ c.goldCreditLimitGrams }}</td>
|
||||
<td><button @click="goToNewOrder(c.id)">New order</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.error {
|
||||
color: #b00020;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
export default _default;
|
||||
@@ -0,0 +1,97 @@
|
||||
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { api } from '../api/client';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
const customers = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(null);
|
||||
const router = useRouter();
|
||||
const session = useSessionStore();
|
||||
onMounted(async () => {
|
||||
try {
|
||||
customers.value = await api.listCustomers();
|
||||
}
|
||||
catch (e) {
|
||||
error.value = e.message;
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
function goToNewOrder(customerId) {
|
||||
session.selectCustomer(customerId);
|
||||
router.push({ name: 'order-new' });
|
||||
}
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
let __VLS_directives;
|
||||
// CSS variable injection
|
||||
// CSS variable injection end
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElements.section)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({});
|
||||
if (__VLS_ctx.loading) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({});
|
||||
}
|
||||
else if (__VLS_ctx.error) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "error" },
|
||||
});
|
||||
(__VLS_ctx.error);
|
||||
}
|
||||
else {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.table, __VLS_intrinsicElements.table)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.thead, __VLS_intrinsicElements.thead)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.tr, __VLS_intrinsicElements.tr)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.tbody, __VLS_intrinsicElements.tbody)({});
|
||||
for (const [c] of __VLS_getVForSourceType((__VLS_ctx.customers))) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.tr, __VLS_intrinsicElements.tr)({
|
||||
key: (c.id),
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
(c.fullName);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
(c.mobile);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
(c.goldDebtGrams);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
(c.fiatDebtRial);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
(c.goldCreditLimitGrams);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!!(__VLS_ctx.loading))
|
||||
return;
|
||||
if (!!(__VLS_ctx.error))
|
||||
return;
|
||||
__VLS_ctx.goToNewOrder(c.id);
|
||||
} },
|
||||
});
|
||||
}
|
||||
}
|
||||
/** @type {__VLS_StyleScopedClasses['error']} */ ;
|
||||
var __VLS_dollars;
|
||||
const __VLS_self = (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
customers: customers,
|
||||
loading: loading,
|
||||
error: error,
|
||||
goToNewOrder: goToNewOrder,
|
||||
};
|
||||
},
|
||||
});
|
||||
export default (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
; /* PartiallyEnd: #4569/main.vue */
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { api, type OrderDetail } from '../api/client';
|
||||
|
||||
const route = useRoute();
|
||||
const order = ref<OrderDetail | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const STATUS_FLOW = [
|
||||
'PLACED',
|
||||
'ADMIN_APPROVED',
|
||||
'SOURCING',
|
||||
'PREPARING',
|
||||
'DELIVERED_TO_AGENT',
|
||||
'DELIVERED_TO_CUSTOMER',
|
||||
];
|
||||
|
||||
async function load() {
|
||||
order.value = await api.getOrder(route.params.id as string);
|
||||
}
|
||||
|
||||
async function advance() {
|
||||
if (!order.value) return;
|
||||
const currentIndex = STATUS_FLOW.indexOf(order.value.status);
|
||||
const next = STATUS_FLOW[currentIndex + 1];
|
||||
if (!next) return;
|
||||
try {
|
||||
order.value = await api.updateOrderStatus(order.value.id, next);
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="order">
|
||||
<h1>Order {{ order.orderNumber }}</h1>
|
||||
<p>Status: <strong>{{ order.status }}</strong></p>
|
||||
<p v-if="order.isSuspendedOverCreditLimit" class="error">Suspended — credit limit exceeded at placement.</p>
|
||||
<p>Total gold weight: {{ order.totalGoldWeightGrams }} g (wage: {{ order.wageWeightGrams }} g)</p>
|
||||
<p>Flat Rial charge: {{ order.flatRialCharge }}</p>
|
||||
<p>Delivered at: {{ order.deliveredAt ?? '—' }}</p>
|
||||
|
||||
<button v-if="STATUS_FLOW.indexOf(order.status) < STATUS_FLOW.length - 1" @click="advance">
|
||||
Advance to next status
|
||||
</button>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<h2>Status history</h2>
|
||||
<ul>
|
||||
<li v-for="(e, i) in order.statusEvents" :key="i">{{ e.toStatus }} — {{ e.createdAt }}</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error {
|
||||
color: #b00020;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
export default _default;
|
||||
@@ -0,0 +1,98 @@
|
||||
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { api } from '../api/client';
|
||||
const route = useRoute();
|
||||
const order = ref(null);
|
||||
const error = ref(null);
|
||||
const STATUS_FLOW = [
|
||||
'PLACED',
|
||||
'ADMIN_APPROVED',
|
||||
'SOURCING',
|
||||
'PREPARING',
|
||||
'DELIVERED_TO_AGENT',
|
||||
'DELIVERED_TO_CUSTOMER',
|
||||
];
|
||||
async function load() {
|
||||
order.value = await api.getOrder(route.params.id);
|
||||
}
|
||||
async function advance() {
|
||||
if (!order.value)
|
||||
return;
|
||||
const currentIndex = STATUS_FLOW.indexOf(order.value.status);
|
||||
const next = STATUS_FLOW[currentIndex + 1];
|
||||
if (!next)
|
||||
return;
|
||||
try {
|
||||
order.value = await api.updateOrderStatus(order.value.id, next);
|
||||
}
|
||||
catch (e) {
|
||||
error.value = e.message;
|
||||
}
|
||||
}
|
||||
onMounted(load);
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
let __VLS_directives;
|
||||
// CSS variable injection
|
||||
// CSS variable injection end
|
||||
if (__VLS_ctx.order) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElements.section)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({});
|
||||
(__VLS_ctx.order.orderNumber);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.strong, __VLS_intrinsicElements.strong)({});
|
||||
(__VLS_ctx.order.status);
|
||||
if (__VLS_ctx.order.isSuspendedOverCreditLimit) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "error" },
|
||||
});
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({});
|
||||
(__VLS_ctx.order.totalGoldWeightGrams);
|
||||
(__VLS_ctx.order.wageWeightGrams);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({});
|
||||
(__VLS_ctx.order.flatRialCharge);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({});
|
||||
(__VLS_ctx.order.deliveredAt ?? '—');
|
||||
if (__VLS_ctx.STATUS_FLOW.indexOf(__VLS_ctx.order.status) < __VLS_ctx.STATUS_FLOW.length - 1) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (__VLS_ctx.advance) },
|
||||
});
|
||||
}
|
||||
if (__VLS_ctx.error) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "error" },
|
||||
});
|
||||
(__VLS_ctx.error);
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.ul, __VLS_intrinsicElements.ul)({});
|
||||
for (const [e, i] of __VLS_getVForSourceType((__VLS_ctx.order.statusEvents))) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.li, __VLS_intrinsicElements.li)({
|
||||
key: (i),
|
||||
});
|
||||
(e.toStatus);
|
||||
(e.createdAt);
|
||||
}
|
||||
}
|
||||
/** @type {__VLS_StyleScopedClasses['error']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['error']} */ ;
|
||||
var __VLS_dollars;
|
||||
const __VLS_self = (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
order: order,
|
||||
error: error,
|
||||
STATUS_FLOW: STATUS_FLOW,
|
||||
advance: advance,
|
||||
};
|
||||
},
|
||||
});
|
||||
export default (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
; /* PartiallyEnd: #4569/main.vue */
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { CreateOrderItemDto } from '@gold-bank/shared-types';
|
||||
import { api, type ProductSummary } from '../api/client';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
|
||||
const session = useSessionStore();
|
||||
const router = useRouter();
|
||||
|
||||
const products = ref<ProductSummary[]>([]);
|
||||
const items = reactive<CreateOrderItemDto[]>([
|
||||
{ productVariantId: '', quantity: 1, wagePercent: 7, discountPercent: 0, flatRialCharge: 0 },
|
||||
]);
|
||||
const submitting = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
products.value = await api.listProducts();
|
||||
});
|
||||
|
||||
function addItem() {
|
||||
items.push({ productVariantId: '', quantity: 1, wagePercent: 7, discountPercent: 0, flatRialCharge: 0 });
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
items.splice(index, 1);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!session.selectedCustomerId) {
|
||||
error.value = 'Select a customer from the dashboard first.';
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const order = await api.createOrder({
|
||||
customerId: session.selectedCustomerId,
|
||||
items: items.map((i) => ({ ...i })),
|
||||
});
|
||||
router.push({ name: 'order-detail', params: { id: order.id } });
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message;
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<h1>New order</h1>
|
||||
<p v-if="!session.selectedCustomerId" class="error">
|
||||
No customer selected — go back to the dashboard and pick one.
|
||||
</p>
|
||||
<p v-else>Customer: {{ session.selectedCustomerId }}</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variant</th>
|
||||
<th>Qty</th>
|
||||
<th>Wage %</th>
|
||||
<th>Discount %</th>
|
||||
<th>Flat Rial</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, idx) in items" :key="idx">
|
||||
<td>
|
||||
<select v-model="item.productVariantId">
|
||||
<option value="" disabled>Select…</option>
|
||||
<template v-for="p in products" :key="p.id">
|
||||
<option v-for="v in p.variants" :key="v.id" :value="v.id">
|
||||
{{ p.title }} — {{ v.title }} ({{ v.rashWeightGrams }}g)
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
</td>
|
||||
<td><input v-model.number="item.quantity" type="number" min="1" /></td>
|
||||
<td><input v-model.number="item.wagePercent" type="number" step="0.1" /></td>
|
||||
<td><input v-model.number="item.discountPercent" type="number" step="0.1" /></td>
|
||||
<td><input v-model.number="item.flatRialCharge" type="number" step="1000" /></td>
|
||||
<td><button @click="removeItem(idx)">Remove</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<button @click="addItem">Add item</button>
|
||||
<br /><br />
|
||||
<button :disabled="submitting" @click="submit">Place order</button>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
.error {
|
||||
color: #b00020;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
export default _default;
|
||||
@@ -0,0 +1,163 @@
|
||||
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { api } from '../api/client';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
const session = useSessionStore();
|
||||
const router = useRouter();
|
||||
const products = ref([]);
|
||||
const items = reactive([
|
||||
{ productVariantId: '', quantity: 1, wagePercent: 7, discountPercent: 0, flatRialCharge: 0 },
|
||||
]);
|
||||
const submitting = ref(false);
|
||||
const error = ref(null);
|
||||
onMounted(async () => {
|
||||
products.value = await api.listProducts();
|
||||
});
|
||||
function addItem() {
|
||||
items.push({ productVariantId: '', quantity: 1, wagePercent: 7, discountPercent: 0, flatRialCharge: 0 });
|
||||
}
|
||||
function removeItem(index) {
|
||||
items.splice(index, 1);
|
||||
}
|
||||
async function submit() {
|
||||
if (!session.selectedCustomerId) {
|
||||
error.value = 'Select a customer from the dashboard first.';
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const order = await api.createOrder({
|
||||
customerId: session.selectedCustomerId,
|
||||
items: items.map((i) => ({ ...i })),
|
||||
});
|
||||
router.push({ name: 'order-detail', params: { id: order.id } });
|
||||
}
|
||||
catch (e) {
|
||||
error.value = e.message;
|
||||
}
|
||||
finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
let __VLS_directives;
|
||||
// CSS variable injection
|
||||
// CSS variable injection end
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElements.section)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({});
|
||||
if (!__VLS_ctx.session.selectedCustomerId) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "error" },
|
||||
});
|
||||
}
|
||||
else {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({});
|
||||
(__VLS_ctx.session.selectedCustomerId);
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.table, __VLS_intrinsicElements.table)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.thead, __VLS_intrinsicElements.thead)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.tr, __VLS_intrinsicElements.tr)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.th, __VLS_intrinsicElements.th)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.tbody, __VLS_intrinsicElements.tbody)({});
|
||||
for (const [item, idx] of __VLS_getVForSourceType((__VLS_ctx.items))) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.tr, __VLS_intrinsicElements.tr)({
|
||||
key: (idx),
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
|
||||
value: (item.productVariantId),
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
|
||||
value: "",
|
||||
disabled: true,
|
||||
});
|
||||
for (const [p] of __VLS_getVForSourceType((__VLS_ctx.products))) {
|
||||
(p.id);
|
||||
for (const [v] of __VLS_getVForSourceType((p.variants))) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
|
||||
key: (v.id),
|
||||
value: (v.id),
|
||||
});
|
||||
(p.title);
|
||||
(v.title);
|
||||
(v.rashWeightGrams);
|
||||
}
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
|
||||
type: "number",
|
||||
min: "1",
|
||||
});
|
||||
(item.quantity);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
|
||||
type: "number",
|
||||
step: "0.1",
|
||||
});
|
||||
(item.wagePercent);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
|
||||
type: "number",
|
||||
step: "0.1",
|
||||
});
|
||||
(item.discountPercent);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
|
||||
type: "number",
|
||||
step: "1000",
|
||||
});
|
||||
(item.flatRialCharge);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.td, __VLS_intrinsicElements.td)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
__VLS_ctx.removeItem(idx);
|
||||
} },
|
||||
});
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (__VLS_ctx.addItem) },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.br)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.br)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (__VLS_ctx.submit) },
|
||||
disabled: (__VLS_ctx.submitting),
|
||||
});
|
||||
if (__VLS_ctx.error) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "error" },
|
||||
});
|
||||
(__VLS_ctx.error);
|
||||
}
|
||||
/** @type {__VLS_StyleScopedClasses['error']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['error']} */ ;
|
||||
var __VLS_dollars;
|
||||
const __VLS_self = (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
session: session,
|
||||
products: products,
|
||||
items: items,
|
||||
submitting: submitting,
|
||||
error: error,
|
||||
addItem: addItem,
|
||||
removeItem: removeItem,
|
||||
submit: submit,
|
||||
};
|
||||
},
|
||||
});
|
||||
export default (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
; /* PartiallyEnd: #4569/main.vue */
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"noEmit": true,
|
||||
"declaration": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./src/app.vue","./src/app.vue.d.ts","./src/env.d.ts","./src/main.ts","./src/api/client.ts","./src/router/index.ts","./src/stores/session.ts","./src/views/dashboardview.vue","./src/views/dashboardview.vue.d.ts","./src/views/orderdetailview.vue","./src/views/orderdetailview.vue.d.ts","./src/views/ordernewview.vue","./src/views/ordernewview.vue.d.ts"],"version":"5.9.3"}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user