feat: initial commit

This commit is contained in:
s.esmaeili
2026-09-09 06:46:27 +03:30
commit 0ed6bed028
214 changed files with 20177 additions and 0 deletions
+12
View File
@@ -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>
+22
View File
@@ -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"
}
}
+23
View File
@@ -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>
+2
View File
@@ -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;
+39
View File
@@ -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 */
+48
View File
@@ -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>;
};
+20
View File
@@ -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 }) }),
};
+61
View File
@@ -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 }) }),
};
+7
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export {};
+5
View File
@@ -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');
+6
View File
@@ -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
View File
@@ -0,0 +1 @@
export declare const router: import("vue-router").Router;
+12
View File
@@ -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 },
],
});
+13
View File
@@ -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
View File
@@ -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;
}>;
+15
View File
@@ -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;
},
},
});
+16
View File
@@ -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>
+2
View File
@@ -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>
+2
View File
@@ -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 */
+112
View File
@@ -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>
+2
View File
@@ -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 */
+14
View File
@@ -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"]
}
+1
View File
@@ -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"}
+12
View File
@@ -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',
},
},
});