Allow editing orders in the order card
Cashiers can edit recipient, TTN, notes, goods and total in the order
modal and save via PATCH /orders/{id}. Edited orders get edited_at and
are no longer overwritten by CRM sync. Editing is blocked once a receipt
exists; changing the TTN resets Nova Poshta tracking fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,8 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.order-modal-meta div {
|
||||
.order-modal-meta div,
|
||||
.order-modal-meta label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
@@ -111,3 +112,154 @@
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.order-modal-edited {
|
||||
margin-left: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.order-modal-notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.order-modal-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-modal-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
textarea.order-modal-input {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.order-modal-input--sku {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.order-modal-input--num {
|
||||
width: 84px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.order-modal-goods td:has(.order-modal-input) {
|
||||
padding: 4px 4px;
|
||||
}
|
||||
|
||||
.order-modal-invalid {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.order-modal-remove {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.order-modal-remove:hover {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.order-modal-add {
|
||||
align-self: flex-start;
|
||||
border: 1px dashed var(--color-border);
|
||||
background: none;
|
||||
color: var(--color-primary);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.order-modal-add:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.order-modal-total {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.order-modal-message {
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-modal-message--ok {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.order-modal-message--error {
|
||||
background: var(--color-danger-bg);
|
||||
color: var(--color-danger);
|
||||
border: 1px solid var(--color-danger-border);
|
||||
}
|
||||
|
||||
.order-modal-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.order-modal-save-btn {
|
||||
border: 1px solid var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.order-modal-save-btn:hover:not(:disabled) {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.order-modal-save-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.order-modal-message--ok {
|
||||
background: rgba(22, 163, 74, 0.18);
|
||||
color: #86efac;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.order-modal {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.order-modal-meta {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.order-modal-goods {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,226 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { updateOrder } from '@/api/orders'
|
||||
import { Modal } from '@/components/Modal'
|
||||
import '@/features/orders/OrderDetailModal.css'
|
||||
import type { Order } from '@/features/orders/types'
|
||||
import type { Order, OrderUpdate } from '@/features/orders/types'
|
||||
import { formatKopecks, toKopecks } from '@/features/receipts/money'
|
||||
import { RECEIPT_STATUS } from '@/features/receipts/types'
|
||||
|
||||
interface OrderDetailModalProps {
|
||||
order: Order
|
||||
/** Кассир/админ и по заказу ещё нет чека — карточку можно править. */
|
||||
canEdit: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function OrderDetailModal({ order, onClose }: OrderDetailModalProps) {
|
||||
interface GoodDraft {
|
||||
key: string
|
||||
id: string | null
|
||||
sku: string
|
||||
name: string
|
||||
price: string
|
||||
quantity: string
|
||||
discount: string
|
||||
}
|
||||
|
||||
interface Draft {
|
||||
recipient_name: string
|
||||
recipient_phone: string
|
||||
recipient_email: string
|
||||
waybill_number: string
|
||||
notes: string
|
||||
total: string
|
||||
goods: GoodDraft[]
|
||||
}
|
||||
|
||||
type TextField = Exclude<keyof Draft, 'goods' | 'total'>
|
||||
|
||||
/** Количество в тысячных (конвенция Checkbox: 1 шт = 1000). */
|
||||
function toThousandths(value: string): number | null {
|
||||
const normalized = value.trim().replace(',', '.')
|
||||
if (!/^\d+(\.\d{1,3})?$/.test(normalized)) return null
|
||||
return Math.round(Number(normalized) * 1000)
|
||||
}
|
||||
|
||||
/** Сумма строки после скидки в копейках; null — в строке некорректные числа. */
|
||||
function lineAmount(good: GoodDraft): number | null {
|
||||
const price = toKopecks(good.price)
|
||||
const quantity = toThousandths(good.quantity)
|
||||
const discount = toKopecks(good.discount)
|
||||
if (price === null || quantity === null || discount === null) return null
|
||||
return Math.round((price * quantity) / 1000) - discount
|
||||
}
|
||||
|
||||
function goodsSum(goods: GoodDraft[]): number | null {
|
||||
let sum = 0
|
||||
for (const good of goods) {
|
||||
const amount = lineAmount(good)
|
||||
if (amount === null) return null
|
||||
sum += amount
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
let newGoodSeq = 0
|
||||
|
||||
function toDraft(order: Order): Draft {
|
||||
return {
|
||||
recipient_name: order.recipient_name ?? '',
|
||||
recipient_phone: order.recipient_phone ?? '',
|
||||
recipient_email: order.recipient_email ?? '',
|
||||
waybill_number: order.waybill_number ?? '',
|
||||
notes: order.notes ?? '',
|
||||
total: order.total_amount,
|
||||
goods: order.goods.map((good) => {
|
||||
// Скидку строки выводим из amount, как бэкенд при сборке чека, — не из discount_amount/percent.
|
||||
const gross = Math.round(((toKopecks(good.price) ?? 0) * (toThousandths(good.quantity) ?? 0)) / 1000)
|
||||
const discount = Math.max(gross - (toKopecks(good.amount) ?? gross), 0)
|
||||
return {
|
||||
key: good.id,
|
||||
id: good.id,
|
||||
sku: good.sku,
|
||||
name: good.name,
|
||||
price: good.price,
|
||||
quantity: good.quantity,
|
||||
discount: formatKopecks(discount),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function orderDiscountOf(draft: Draft): number {
|
||||
return (goodsSum(draft.goods) ?? 0) - (toKopecks(draft.total) ?? 0)
|
||||
}
|
||||
|
||||
/** Проверяет черновик и собирает тело PATCH; строка — текст ошибки для кассира. */
|
||||
function toPayload(draft: Draft): OrderUpdate | string {
|
||||
if (draft.goods.length === 0) return 'Добавьте хотя бы один товар'
|
||||
const goods = []
|
||||
for (const good of draft.goods) {
|
||||
const label = good.name.trim() || 'без названия'
|
||||
if (!good.name.trim()) return 'У товара не указано наименование'
|
||||
const price = toKopecks(good.price)
|
||||
const quantity = toThousandths(good.quantity)
|
||||
const discount = toKopecks(good.discount)
|
||||
if (price === null) return `Товар «${label}»: некорректная цена`
|
||||
if (quantity === null || quantity <= 0) return `Товар «${label}»: некорректное количество`
|
||||
if (discount === null) return `Товар «${label}»: некорректная скидка`
|
||||
if ((lineAmount(good) ?? 0) < 0) return `Товар «${label}»: скидка больше суммы строки`
|
||||
goods.push({
|
||||
id: good.id,
|
||||
sku: good.sku.trim(),
|
||||
name: good.name.trim(),
|
||||
price: formatKopecks(price),
|
||||
quantity: (quantity / 1000).toFixed(3),
|
||||
discount_amount: formatKopecks(discount),
|
||||
})
|
||||
}
|
||||
const total = toKopecks(draft.total)
|
||||
if (total === null) return 'Некорректная сумма заказа'
|
||||
const sum = goodsSum(draft.goods) ?? 0
|
||||
if (total > sum) return `Сумма заказа больше суммы товаров (${formatKopecks(sum)} ₴)`
|
||||
|
||||
return {
|
||||
recipient_name: draft.recipient_name,
|
||||
recipient_phone: draft.recipient_phone,
|
||||
recipient_email: draft.recipient_email,
|
||||
waybill_number: draft.waybill_number,
|
||||
notes: draft.notes,
|
||||
total_amount: formatKopecks(total),
|
||||
goods,
|
||||
}
|
||||
}
|
||||
|
||||
const TEXT_FIELDS: { field: TextField; label: string; type?: string }[] = [
|
||||
{ field: 'recipient_name', label: 'Клиент' },
|
||||
{ field: 'recipient_phone', label: 'Телефон', type: 'tel' },
|
||||
{ field: 'recipient_email', label: 'Email', type: 'email' },
|
||||
{ field: 'waybill_number', label: 'Номер ТТН' },
|
||||
]
|
||||
|
||||
export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const [draft, setDraft] = useState<Draft>(() => toDraft(order))
|
||||
const [saved, setSaved] = useState<Draft>(draft)
|
||||
// Скидка на весь заказ (итог товаров − сумма заказа): держим её при правке товаров.
|
||||
const [orderDiscount, setOrderDiscount] = useState(() => orderDiscountOf(draft))
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null)
|
||||
|
||||
const dirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(saved), [draft, saved])
|
||||
const editable = canEdit && !order.has_receipt
|
||||
|
||||
function requestClose() {
|
||||
if (dirty && !window.confirm('Есть несохранённые изменения. Закрыть без сохранения?')) return
|
||||
onClose()
|
||||
}
|
||||
|
||||
function setField(field: TextField, value: string) {
|
||||
setDraft((prev) => ({ ...prev, [field]: value }))
|
||||
setMessage(null)
|
||||
}
|
||||
|
||||
/** Меняет товары и пересчитывает итог с сохранением скидки на заказ. */
|
||||
function setGoods(update: (goods: GoodDraft[]) => GoodDraft[]) {
|
||||
const goods = update(draft.goods)
|
||||
const sum = goodsSum(goods)
|
||||
setDraft({ ...draft, goods, total: sum === null ? draft.total : formatKopecks(Math.max(sum - orderDiscount, 0)) })
|
||||
setMessage(null)
|
||||
}
|
||||
|
||||
function setTotal(value: string) {
|
||||
setDraft({ ...draft, total: value })
|
||||
const sum = goodsSum(draft.goods)
|
||||
const total = toKopecks(value)
|
||||
if (sum !== null && total !== null) setOrderDiscount(sum - total)
|
||||
setMessage(null)
|
||||
}
|
||||
|
||||
function setGood(key: string, field: keyof GoodDraft, value: string) {
|
||||
setGoods((goods) => goods.map((good) => (good.key === key ? { ...good, [field]: value } : good)))
|
||||
}
|
||||
|
||||
function addGood() {
|
||||
newGoodSeq += 1
|
||||
setGoods((goods) => [
|
||||
...goods,
|
||||
{ key: `new-${newGoodSeq}`, id: null, sku: '', name: '', price: '0.00', quantity: '1', discount: '0.00' },
|
||||
])
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const payload = toPayload(draft)
|
||||
if (typeof payload === 'string') {
|
||||
setMessage({ tone: 'error', text: payload })
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
const updated = await updateOrder(order.id, payload)
|
||||
const next = toDraft(updated)
|
||||
setDraft(next)
|
||||
setSaved(next)
|
||||
setOrderDiscount(orderDiscountOf(next))
|
||||
setMessage({ tone: 'ok', text: 'Изменения сохранены' })
|
||||
await queryClient.invalidateQueries({ queryKey: ['orders'] })
|
||||
} catch (err) {
|
||||
setMessage({ tone: 'error', text: err instanceof Error ? err.message : 'Не удалось сохранить заказ' })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose}>
|
||||
<Modal onClose={requestClose} wide={editable}>
|
||||
<div className="order-modal">
|
||||
<div className="order-modal-header">
|
||||
<h3>Просмотр заказа {order.id}</h3>
|
||||
<button type="button" className="order-modal-close" onClick={onClose} aria-label="Закрыть">
|
||||
<h3>
|
||||
Заказ {order.id}
|
||||
{order.edited && <span className="order-modal-edited">изменён вручную</span>}
|
||||
</h3>
|
||||
<button type="button" className="order-modal-close" onClick={requestClose} aria-label="Закрыть">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
@@ -24,10 +230,6 @@ export function OrderDetailModal({ order, onClose }: OrderDetailModalProps) {
|
||||
<span className="order-modal-label">Дата</span>
|
||||
<span>{order.create_date_time}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="order-modal-label">Номер ТТН</span>
|
||||
<span>{order.waybill_number || '—'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="order-modal-label">Статус ТТН</span>
|
||||
<span>{order.np_status || 'Нет данных'}</span>
|
||||
@@ -46,27 +248,45 @@ export function OrderDetailModal({ order, onClose }: OrderDetailModalProps) {
|
||||
: '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="order-modal-label">Клиент</span>
|
||||
<span>{order.recipient_name || '—'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="order-modal-label">Телефон</span>
|
||||
<span>{order.recipient_phone || '—'}</span>
|
||||
</div>
|
||||
{order.recipient_email && (
|
||||
<div>
|
||||
<span className="order-modal-label">Email</span>
|
||||
<span>{order.recipient_email}</span>
|
||||
</div>
|
||||
{TEXT_FIELDS.map(({ field, label, type }) =>
|
||||
editable ? (
|
||||
<label key={field}>
|
||||
<span className="order-modal-label">{label}</span>
|
||||
<input
|
||||
type={type ?? 'text'}
|
||||
className="order-modal-input"
|
||||
value={draft[field]}
|
||||
onChange={(e) => setField(field, e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
(field !== 'recipient_email' || draft[field]) && (
|
||||
<div key={field}>
|
||||
<span className="order-modal-label">{label}</span>
|
||||
<span>{draft[field] || '—'}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
{order.notes && (
|
||||
<div className="order-modal-notes">
|
||||
{editable ? (
|
||||
<label className="order-modal-notes">
|
||||
<span className="order-modal-label">Заметки</span>
|
||||
<p>{order.notes}</p>
|
||||
</div>
|
||||
<textarea
|
||||
className="order-modal-input"
|
||||
rows={2}
|
||||
value={draft.notes}
|
||||
onChange={(e) => setField('notes', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
draft.notes && (
|
||||
<div className="order-modal-notes">
|
||||
<span className="order-modal-label">Заметки</span>
|
||||
<p>{draft.notes}</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<table className="order-modal-goods">
|
||||
@@ -78,22 +298,71 @@ export function OrderDetailModal({ order, onClose }: OrderDetailModalProps) {
|
||||
<th>Кол-во</th>
|
||||
<th>Скидка</th>
|
||||
<th>Сумма</th>
|
||||
{editable && <th aria-label="Удалить" />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{order.goods.map((good) => (
|
||||
<tr key={good.id}>
|
||||
<td>{good.name}</td>
|
||||
<td>{good.sku}</td>
|
||||
<td>{good.price}</td>
|
||||
<td>{good.quantity}</td>
|
||||
<td>{good.discount_amount && good.discount_amount !== '0.00' ? good.discount_amount : '—'}</td>
|
||||
<td>{good.amount}</td>
|
||||
</tr>
|
||||
))}
|
||||
{order.goods.length === 0 && (
|
||||
{draft.goods.map((good) => {
|
||||
const amount = lineAmount(good)
|
||||
return (
|
||||
<tr key={good.key}>
|
||||
{editable ? (
|
||||
<>
|
||||
<td>
|
||||
<input
|
||||
className="order-modal-input"
|
||||
value={good.name}
|
||||
onChange={(e) => setGood(good.key, 'name', e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
className="order-modal-input order-modal-input--sku"
|
||||
value={good.sku}
|
||||
onChange={(e) => setGood(good.key, 'sku', e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
{(['price', 'quantity', 'discount'] as const).map((field) => (
|
||||
<td key={field}>
|
||||
<input
|
||||
className="order-modal-input order-modal-input--num"
|
||||
inputMode="decimal"
|
||||
value={good[field]}
|
||||
onChange={(e) => setGood(good.key, field, e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<td>{good.name}</td>
|
||||
<td>{good.sku}</td>
|
||||
<td>{good.price}</td>
|
||||
<td>{good.quantity}</td>
|
||||
<td>{good.discount !== '0.00' ? good.discount : '—'}</td>
|
||||
</>
|
||||
)}
|
||||
<td className={amount === null || amount < 0 ? 'order-modal-invalid' : undefined}>
|
||||
{amount === null ? '—' : formatKopecks(amount)}
|
||||
</td>
|
||||
{editable && (
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
className="order-modal-remove"
|
||||
aria-label={`Удалить товар ${good.name}`}
|
||||
onClick={() => setGoods((goods) => goods.filter((g) => g.key !== good.key))}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{draft.goods.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="order-modal-goods-empty">
|
||||
<td colSpan={editable ? 7 : 6} className="order-modal-goods-empty">
|
||||
Товары не указаны
|
||||
</td>
|
||||
</tr>
|
||||
@@ -101,9 +370,28 @@ export function OrderDetailModal({ order, onClose }: OrderDetailModalProps) {
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{editable && (
|
||||
<button type="button" className="order-modal-add" onClick={addGood}>
|
||||
+ Добавить товар
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="order-modal-total">
|
||||
<span>Итого</span>
|
||||
<span>{order.total_amount} ₴</span>
|
||||
{editable ? (
|
||||
<span>
|
||||
<input
|
||||
className="order-modal-input order-modal-input--num"
|
||||
inputMode="decimal"
|
||||
value={draft.total}
|
||||
title="Сумма заказа после всех скидок"
|
||||
onChange={(e) => setTotal(e.target.value)}
|
||||
/>{' '}
|
||||
₴
|
||||
</span>
|
||||
) : (
|
||||
<span>{order.total_amount} ₴</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{order.receipt_status && (
|
||||
@@ -127,8 +415,20 @@ export function OrderDetailModal({ order, onClose }: OrderDetailModalProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message && <div className={`order-modal-message order-modal-message--${message.tone}`}>{message.text}</div>}
|
||||
|
||||
<div className="order-modal-actions">
|
||||
<button type="button" className="order-modal-close-btn" onClick={onClose}>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="order-modal-save-btn"
|
||||
disabled={!dirty || saving}
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
{saving ? 'Сохранение…' : 'Сохранить'}
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="order-modal-close-btn" onClick={requestClose}>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -36,4 +36,26 @@ export interface Order {
|
||||
receipt_status: ReceiptStatus | null
|
||||
receipt_error: string | null
|
||||
receipt_prepayment: string | null
|
||||
// Заказ правили вручную — синхронизация из CRM его больше не перезаписывает.
|
||||
edited: boolean
|
||||
}
|
||||
|
||||
/** Тело `PATCH /orders/{id}` (`OrderUpdateIn`). Деньги — строки "1200.00", `amount` считает сервер. */
|
||||
export interface OrderGoodUpdate {
|
||||
id: string | null
|
||||
sku: string
|
||||
name: string
|
||||
price: string
|
||||
quantity: string
|
||||
discount_amount: string
|
||||
}
|
||||
|
||||
export interface OrderUpdate {
|
||||
recipient_name: string
|
||||
recipient_phone: string
|
||||
recipient_email: string
|
||||
waybill_number: string
|
||||
notes: string
|
||||
total_amount: string
|
||||
goods: OrderGoodUpdate[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user