diff --git a/apps/internal-ui/src/features/sales/orders/SalesOrdersHeader.tsx b/apps/internal-ui/src/features/sales/orders/SalesOrdersHeader.tsx new file mode 100644 index 0000000..d9c0b8f --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/SalesOrdersHeader.tsx @@ -0,0 +1,19 @@ +import { Icon } from "../../../shared/icons"; + +export function SalesOrdersHeader({ query, setQuery }: { query: string; setQuery: (value: string) => void }) { + return ( +
+
+

Продажи

+

Заказы, платежи, возвраты, подписки и исполнение · основной объект — заказ

+
+
+ + +
+
+ ); +} diff --git a/apps/internal-ui/src/features/sales/orders/SalesOrdersPage.tsx b/apps/internal-ui/src/features/sales/orders/SalesOrdersPage.tsx new file mode 100644 index 0000000..27f6687 --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/SalesOrdersPage.tsx @@ -0,0 +1,32 @@ +import { useMemo, useState } from "react"; + +import { salesOrdersData, salesOrderTabs } from "./model"; +import { SalesOrdersHeader } from "./SalesOrdersHeader"; +import { SalesOrdersPagination } from "./SalesOrdersPagination"; +import { SalesOrdersTableCard } from "./SalesOrdersTableCard"; +import { SalesOrdersTabs } from "./SalesOrdersTabs"; +import type { SalesOrdersTab } from "./types"; + +export function SalesOrdersPage() { + const [tab, setTab] = useState("orders"); + const [query, setQuery] = useState(""); + const rows = useMemo(() => { + const value = query.trim().toLowerCase(); + return salesOrdersData[tab].filter((row) => !value || row.search.includes(value)); + }, [query, tab]); + const total = salesOrderTabs.find((item) => item.key === tab)?.count ?? rows.length; + + function selectTab(nextTab: SalesOrdersTab) { + setTab(nextTab); + setQuery(""); + } + + return ( + <> + + + + + + ); +} diff --git a/apps/internal-ui/src/features/sales/orders/SalesOrdersPagination.tsx b/apps/internal-ui/src/features/sales/orders/SalesOrdersPagination.tsx new file mode 100644 index 0000000..dc5fbeb --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/SalesOrdersPagination.tsx @@ -0,0 +1,15 @@ +export function SalesOrdersPagination({ shown, total }: { shown: number; total: number }) { + return ( +
+
Показано {shown} из {total}
+
+ + + + + + 20 / стр. +
+
+ ); +} diff --git a/apps/internal-ui/src/features/sales/orders/SalesOrdersTableCard.tsx b/apps/internal-ui/src/features/sales/orders/SalesOrdersTableCard.tsx new file mode 100644 index 0000000..2dd882d --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/SalesOrdersTableCard.tsx @@ -0,0 +1,32 @@ +import { Icon } from "../../../shared/icons"; +import { FulfillmentTable } from "./tables/FulfillmentTable"; +import { OrdersTable } from "./tables/OrdersTable"; +import { PaymentsTable } from "./tables/PaymentsTable"; +import { RefundsTable } from "./tables/RefundsTable"; +import { SubscriptionsTable } from "./tables/SubscriptionsTable"; +import type { SalesFulfillment, SalesOrder, SalesOrdersTab, SalesPayment, SalesRefund, SalesSubscription } from "./types"; + +export function SalesOrdersTableCard({ activeTab, rows }: { activeTab: SalesOrdersTab; rows: Array<{ search: string }> }) { + return ( +
+
+ {activeTab === "orders" && } + {activeTab === "subs" && } + {activeTab === "pays" && } + {activeTab === "refunds" && } + {activeTab === "exec" && } + {rows.length === 0 && } +
+
+ ); +} + +function SalesOrdersEmpty() { + return ( +
+
+ Ничего не найдено + Измените поисковый запрос. +
+ ); +} diff --git a/apps/internal-ui/src/features/sales/orders/SalesOrdersTabs.tsx b/apps/internal-ui/src/features/sales/orders/SalesOrdersTabs.tsx new file mode 100644 index 0000000..5cb851d --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/SalesOrdersTabs.tsx @@ -0,0 +1,14 @@ +import { salesOrderTabs } from "./model"; +import type { SalesOrdersTab } from "./types"; + +export function SalesOrdersTabs({ activeTab, setActiveTab }: { activeTab: SalesOrdersTab; setActiveTab: (tab: SalesOrdersTab) => void }) { + return ( +
+ {salesOrderTabs.map((tab) => ( + + ))} +
+ ); +} diff --git a/apps/internal-ui/src/features/sales/orders/StatusBadge.tsx b/apps/internal-ui/src/features/sales/orders/StatusBadge.tsx new file mode 100644 index 0000000..9c5e2ee --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/StatusBadge.tsx @@ -0,0 +1,9 @@ +import type { StatusBadge as StatusBadgeValue } from "./types"; + +export function StatusBadge({ value }: { value: StatusBadgeValue }) { + return {value.label}; +} + +export function MonoLink({ children }: { children: string }) { + return event.preventDefault()}>{children}; +} diff --git a/apps/internal-ui/src/features/sales/orders/model.ts b/apps/internal-ui/src/features/sales/orders/model.ts new file mode 100644 index 0000000..19d5376 --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/model.ts @@ -0,0 +1,78 @@ +import type { SalesFulfillment, SalesOrder, SalesOrdersTab, SalesPayment, SalesRefund, SalesSubscription, StatusTone } from "./types"; + +const statusTone = { + green: { color: "#389e0d", bg: "#f6ffed" }, + amber: { color: "#d48806", bg: "#fffbe6" }, + red: { color: "#cf1322", bg: "#fff2f0" }, + blue: { color: "#0958d9", bg: "#e6f4ff" }, + gray: { color: "#8c8c8c", bg: "#f5f5f5" }, + purple: { color: "#722ed1", bg: "#f9f0ff" }, +} satisfies Record; + +const sourceColor = { + MAX: "#6b5be0", + Telegram: "#2f8fd0", + Web: "#0f9b8e", +} satisfies Record; + +export const salesOrderTabs: Array<{ key: SalesOrdersTab; label: string; count: number }> = [ + { key: "orders", label: "Заказы", count: 6 }, + { key: "subs", label: "Подписки", count: 4 }, + { key: "pays", label: "Платежи", count: 5 }, + { key: "refunds", label: "Возвраты", count: 3 }, + { key: "exec", label: "Исполнение", count: 4 }, +]; + +function badge(value: `${StatusTone}:${string}`) { + const [tone, label] = value.split(":") as [StatusTone, string]; + return { ...statusTone[tone], label }; +} + +function search(...parts: string[]) { + return parts.join(" ").toLowerCase(); +} + +export const salesOrders: SalesOrder[] = [ + { id: "ORD-10519", date: "сегодня 14:02", client: "Татьяна Лебедева", offer: "FirePage · Business", amount: "₽2 490", pay: badge("green:Оплачен"), fulfillment: badge("green:Исполнен"), source: { label: "MAX", color: sourceColor.MAX }, seller: "AI-агент", sellerAI: true, search: search("ORD-10519", "Татьяна Лебедева") }, + { id: "ORD-10518", date: "сегодня 13:40", client: "Анна Морозова", offer: "Foxray · Pro", amount: "₽990", pay: badge("green:Оплачен"), fulfillment: badge("green:Исполнен"), source: { label: "Telegram", color: sourceColor.Telegram }, seller: "Анна Котова", search: search("ORD-10518", "Анна Морозова") }, + { id: "ORD-10516", date: "сегодня 12:18", client: "Павел Новиков", offer: "FirePage · Business", amount: "₽2 490", pay: badge("green:Оплачен"), fulfillment: badge("blue:В процессе"), source: { label: "MAX", color: sourceColor.MAX }, seller: "AI-агент", sellerAI: true, search: search("ORD-10516", "Павел Новиков") }, + { id: "ORD-10512", date: "сегодня 11:05", client: "Елена Кузнецова", offer: "Foxray · Team", amount: "₽4 980", pay: badge("amber:Ожидает"), fulfillment: badge("gray:—"), source: { label: "Web", color: sourceColor.Web }, seller: "Иван Петров", search: search("ORD-10512", "Елена Кузнецова") }, + { id: "ORD-10508", date: "вчера 18:22", client: "Игорь Соколов", offer: "Foxray · Pro", amount: "₽990", pay: badge("gray:Возврат"), fulfillment: badge("green:Исполнен"), source: { label: "MAX", color: sourceColor.MAX }, seller: "AI-агент", sellerAI: true, search: search("ORD-10508", "Игорь Соколов") }, + { id: "ORD-10482", date: "вчера 16:50", client: "Сергей Волков", offer: "FirePage · Business", amount: "₽2 490", pay: badge("red:Не оплачен"), fulfillment: badge("gray:—"), source: { label: "Telegram", color: sourceColor.Telegram }, seller: "AI-агент", sellerAI: true, search: search("ORD-10482", "Сергей Волков") }, +]; + +export const salesSubscriptions: SalesSubscription[] = [ + { id: "SUB-2041", client: "Татьяна Лебедева", offer: "FirePage Business · мес", status: badge("green:Активна"), period: "16 июн — 16 июл", next: "16 июл 2026", entitlement: "10 мест", search: search("SUB-2041", "Татьяна Лебедева") }, + { id: "SUB-2038", client: "Анна Морозова", offer: "Foxray Pro · мес", status: badge("green:Активна"), period: "02 июн — 02 июл", next: "02 июл 2026", entitlement: "1 место", search: search("SUB-2038", "Анна Морозова") }, + { id: "SUB-2025", client: "Игорь Соколов", offer: "Foxray Pro · мес", status: badge("gray:Отменена"), period: "—", next: "—", entitlement: "—", search: search("SUB-2025", "Игорь Соколов") }, + { id: "SUB-2011", client: "Елена Кузнецова", offer: "Foxray Team · год", status: badge("amber:Приостановлена"), period: "10 мар — 10 мар 27", next: "пауза", entitlement: "5 мест", search: search("SUB-2011", "Елена Кузнецова") }, +]; + +export const salesPayments: SalesPayment[] = [ + { id: "PAY-88231", order: "ORD-10519", provider: "Точка", amount: "₽2 490", status: badge("green:Успешно"), method: "Карта ···4242", date: "сегодня 14:02", reconcile: { color: statusTone.green.color, label: "Сверено" }, search: search("PAY-88231", "ORD-10519") }, + { id: "PAY-88230", order: "ORD-10518", provider: "Точка", amount: "₽990", status: badge("green:Успешно"), method: "СБП", date: "сегодня 13:40", reconcile: { color: statusTone.green.color, label: "Сверено" }, search: search("PAY-88230", "ORD-10518") }, + { id: "PAY-88224", order: "ORD-10512", provider: "Точка", amount: "₽4 980", status: badge("amber:Ожидает"), method: "Карта", date: "сегодня 11:05", reconcile: { color: statusTone.amber.color, label: "Ожидает сверки" }, search: search("PAY-88224", "ORD-10512") }, + { id: "PAY-88210", order: "ORD-10508", provider: "Точка", amount: "₽990", status: badge("gray:Возврат"), method: "Карта ···1188", date: "вчера 18:25", reconcile: { color: statusTone.green.color, label: "Сверено" }, search: search("PAY-88210", "ORD-10508") }, + { id: "PAY-88198", order: "ORD-10482", provider: "Точка", amount: "₽2 490", status: badge("red:Отклонён"), method: "Карта ···7701", date: "вчера 16:51", reconcile: { color: statusTone.gray.color, label: "—" }, search: search("PAY-88198", "ORD-10482") }, +]; + +export const salesRefunds: SalesRefund[] = [ + { id: "REF-3120", reference: "ORD-10508 · PAY-88210", amount: "₽990", reason: "Запрос клиента", status: badge("green:Завершён"), actor: "Иван Петров", date: "сегодня 09:14", search: search("REF-3120", "ORD-10508 · PAY-88210") }, + { id: "REF-3118", reference: "ORD-10455 · PAY-87990", amount: "₽2 490", reason: "Дубль оплаты", status: badge("amber:В обработке"), actor: "OWNER", date: "вчера 20:02", search: search("REF-3118", "ORD-10455 · PAY-87990") }, + { id: "REF-3101", reference: "ORD-10390 · PAY-87740", amount: "₽4 980", reason: "Брак поставки", status: badge("red:Отклонён"), actor: "Иван Петров", date: "13 июн", search: search("REF-3101", "ORD-10390 · PAY-87740") }, +]; + +export const salesFulfillment: SalesFulfillment[] = [ + { order: "ORD-10519", product: "FirePage", operation: "OP-55120", status: badge("green:Успешно"), attempts: "1", error: "—", updated: "сегодня 14:03", canRetry: false, search: search("ORD-10519", "OP-55120") }, + { order: "ORD-10516", product: "FirePage", operation: "OP-55117", status: badge("blue:В процессе"), attempts: "1", error: "—", updated: "сегодня 12:19", canRetry: false, search: search("ORD-10516", "OP-55117") }, + { order: "ORD-10512", product: "Foxray", operation: "OP-55110", status: badge("amber:Ожидает оплаты"), attempts: "0", error: "—", updated: "сегодня 11:05", canRetry: false, search: search("ORD-10512", "OP-55110") }, + { order: "ORD-10470", product: "Foxray", operation: "OP-55088", status: badge("red:Ошибка"), attempts: "3", error: "TIMEOUT: provider 504", updated: "вчера 15:40", canRetry: true, search: search("ORD-10470", "OP-55088") }, +]; + +export const salesOrdersData = { + orders: salesOrders, + subs: salesSubscriptions, + pays: salesPayments, + refunds: salesRefunds, + exec: salesFulfillment, +}; diff --git a/apps/internal-ui/src/features/sales/orders/styles.css b/apps/internal-ui/src/features/sales/orders/styles.css new file mode 100644 index 0000000..672780e --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/styles.css @@ -0,0 +1,4 @@ +@import "./styles/header.css"; +@import "./styles/tabs.css"; +@import "./styles/table.css"; +@import "./styles/pagination.css"; diff --git a/apps/internal-ui/src/features/sales/orders/styles/header.css b/apps/internal-ui/src/features/sales/orders/styles/header.css new file mode 100644 index 0000000..6e60f98 --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/styles/header.css @@ -0,0 +1,80 @@ +.sales-orders-page { + width: min(1340px, 100%); + max-width: 1340px; +} + +.sales-orders-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + margin-bottom: 18px; +} + +.sales-orders-header h1 { + margin: 0; + color: #1f1f1f; + font-size: 24px; + font-weight: 700; + letter-spacing: 0; + line-height: 1.2; +} + +.sales-orders-header p { + margin: 6px 0 0; + color: #8c8c8c; + font-size: 13.5px; +} + +.sales-orders-header-actions { + display: flex; + align-items: center; + gap: 9px; + flex: none; +} + +.sales-orders-search { + width: 240px; + height: 36px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 12px; + color: #bfbfbf; + background: #ffffff; + border: 1px solid #e8e8e8; + border-radius: 8px; +} + +.sales-orders-search:focus-within { + border-color: #1677ff; +} + +.sales-orders-search input { + width: 100%; + color: #262626; + background: transparent; + border: 0; + outline: 0; + font: inherit; + font-size: 13px; +} + +.sales-orders-header-actions button { + height: 36px; + display: inline-flex; + align-items: center; + gap: 7px; + padding: 0 14px; + color: #262626; + background: #ffffff; + border: 1px solid #d9d9d9; + border-radius: 8px; + font-size: 13px; + font-weight: 500; +} + +.sales-orders-header-actions button:hover { + color: #1677ff; + border-color: #1677ff; +} diff --git a/apps/internal-ui/src/features/sales/orders/styles/pagination.css b/apps/internal-ui/src/features/sales/orders/styles/pagination.css new file mode 100644 index 0000000..831dd09 --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/styles/pagination.css @@ -0,0 +1,53 @@ +.sales-orders-pagination { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 18px; + background: #ffffff; + border: 1px solid #f0f0f0; + border-top: 0; + border-radius: 0 0 12px 12px; +} + +.sales-orders-pagination > div:first-child, +.sales-orders-pagination b { + color: #8c8c8c; + font-size: 12.5px; + font-weight: 400; +} + +.sales-orders-pagination > div:last-child { + display: flex; + align-items: center; + gap: 6px; +} + +.sales-orders-pagination button { + min-width: 30px; + height: 30px; + padding: 0 9px; + color: #595959; + background: #ffffff; + border: 1px solid #e8e8e8; + border-radius: 7px; + font-size: 13px; +} + +.sales-orders-pagination button.active { + color: #ffffff; + background: #1677ff; + border-color: #1677ff; + font-weight: 600; +} + +.sales-orders-pagination button:disabled { + color: #bfbfbf; + cursor: not-allowed; +} + +.sales-orders-pagination span { + width: 1px; + height: 20px; + margin: 0 4px; + background: #f0f0f0; +} diff --git a/apps/internal-ui/src/features/sales/orders/styles/table.css b/apps/internal-ui/src/features/sales/orders/styles/table.css new file mode 100644 index 0000000..d1d7130 --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/styles/table.css @@ -0,0 +1,215 @@ +.sales-orders-card { + overflow: hidden; + background: #ffffff; + border: 1px solid #f0f0f0; + border-radius: 12px 12px 0 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); +} + +.sales-orders-table-scroll { + overflow-x: auto; +} + +.sales-orders-table { + width: 100%; + border-collapse: collapse; + color: #595959; + font-size: 13px; +} + +.sales-orders-table.orders { + min-width: 1180px; +} + +.sales-orders-table.subscriptions, +.sales-orders-table.fulfillment { + min-width: 1080px; +} + +.sales-orders-table.payments { + min-width: 1120px; +} + +.sales-orders-table.refunds { + min-width: 1000px; +} + +.sales-orders-table th { + padding: 11px 10px; + color: #8c8c8c; + background: #fafafa; + border-bottom: 1px solid #f0f0f0; + font-size: 11px; + font-weight: 600; + text-align: left; +} + +.sales-orders-table th:first-child, +.sales-orders-table td:first-child { + padding-left: 16px; +} + +.sales-orders-table td { + padding: 12px 10px; + border-bottom: 1px solid #f5f5f5; +} + +.sales-orders-table tr:hover td { + background: #fafbfc; +} + +.sales-orders-table .numeric { + text-align: right; +} + +.sales-orders-table .nowrap { + white-space: nowrap; +} + +.sales-orders-table .strong-text, +.sales-orders-table strong { + color: #262626; +} + +.sales-orders-mono-link, +.mono-strong, +.mono-muted { + font-family: "SF Mono", ui-monospace, Menlo, monospace; + font-size: 12.5px; +} + +.sales-orders-mono-link { + color: #1677ff; + font-weight: 600; + text-decoration: none; +} + +.mono-strong { + color: #262626; + font-weight: 600; +} + +.mono-muted { + color: #595959; + font-size: 12px; +} + +.sales-orders-status { + display: inline-flex; + align-items: center; + padding: 2px 9px; + border-radius: 6px; + font-size: 11.5px; + font-weight: 600; + white-space: nowrap; +} + +.sales-orders-source { + display: inline-flex; + align-items: center; + gap: 5px; + color: #595959; + font-size: 12px; +} + +.sales-orders-source i { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.seller, +.seller-ai { + font-size: 12.5px; + font-weight: 500; +} + +.seller { + color: #262626; +} + +.seller-ai { + color: #722ed1; +} + +.sales-orders-table .row-actions { + width: 44px; + text-align: center; +} + +.sales-orders-table .row-actions button { + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + color: #8c8c8c; + background: transparent; + border: 0; + border-radius: 7px; +} + +.sales-orders-table .row-actions button:hover { + color: #262626; + background: #f0f0f0; +} + +.attempts.danger, +.mono-muted.error { + color: #cf1322; + font-weight: 600; +} + +.sales-orders-retry { + height: 30px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 12px; + color: #cf1322; + background: #ffffff; + border: 1px solid #ffccc7; + border-radius: 7px; + font-size: 12.5px; + font-weight: 600; +} + +.sales-orders-retry:hover { + background: #fff2f0; +} + +.muted-dash { + color: #bfbfbf; + font-size: 12px; +} + +.sales-orders-empty { + padding: 54px 20px; + text-align: center; +} + +.sales-orders-empty div { + width: 48px; + height: 48px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-bottom: 14px; + color: #bfbfbf; + background: #f5f5f5; + border-radius: 50%; +} + +.sales-orders-empty strong { + display: block; + color: #595959; + font-size: 14px; + font-weight: 600; +} + +.sales-orders-empty span { + display: block; + margin-top: 4px; + color: #bfbfbf; + font-size: 12.5px; +} diff --git a/apps/internal-ui/src/features/sales/orders/styles/tabs.css b/apps/internal-ui/src/features/sales/orders/styles/tabs.css new file mode 100644 index 0000000..24d11ba --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/styles/tabs.css @@ -0,0 +1,54 @@ +.sales-orders-tabs { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 18px; + border-bottom: 1px solid #e8e8e8; +} + +.sales-orders-tabs button { + position: relative; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 11px 16px; + color: #595959; + background: transparent; + border: 0; + font-size: 14px; + font-weight: 500; +} + +.sales-orders-tabs button.active { + color: #1677ff; + font-weight: 600; +} + +.sales-orders-tabs button.active::after { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 2px; + background: #1677ff; + content: ""; +} + +.sales-orders-tabs span { + min-width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 6px; + color: #8c8c8c; + background: #f0f0f0; + border-radius: 10px; + font-size: 11px; + font-weight: 700; +} + +.sales-orders-tabs button.active span { + color: #1677ff; + background: #e6f4ff; +} diff --git a/apps/internal-ui/src/features/sales/orders/tables/FulfillmentTable.tsx b/apps/internal-ui/src/features/sales/orders/tables/FulfillmentTable.tsx new file mode 100644 index 0000000..608b748 --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/tables/FulfillmentTable.tsx @@ -0,0 +1,30 @@ +import { Icon } from "../../../../shared/icons"; +import { MonoLink, StatusBadge } from "../StatusBadge"; +import type { SalesFulfillment } from "../types"; + +export function FulfillmentTable({ rows }: { rows: SalesFulfillment[] }) { + if (rows.length === 0) return null; + return ( + + + + + + + + {rows.map((row) => ( + + + + + + + + + + + ))} + +
ЗАКАЗПРОДУКТОПЕРАЦИЯСТАТУСПОПЫТКИПОСЛ. ОШИБКАОБНОВЛЕНОДЕЙСТВИЕ
{row.order}{row.product}{row.operation}{row.attempts}{row.error}{row.updated}{row.canRetry ? : —}
+ ); +} diff --git a/apps/internal-ui/src/features/sales/orders/tables/OrdersTable.tsx b/apps/internal-ui/src/features/sales/orders/tables/OrdersTable.tsx new file mode 100644 index 0000000..9ca3ac7 --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/tables/OrdersTable.tsx @@ -0,0 +1,33 @@ +import { Icon } from "../../../../shared/icons"; +import { MonoLink, StatusBadge } from "../StatusBadge"; +import type { SalesOrder } from "../types"; + +export function OrdersTable({ rows }: { rows: SalesOrder[] }) { + if (rows.length === 0) return null; + return ( + + + + + + + + {rows.map((row) => ( + + + + + + + + + + + + + ))} + +
ЗАКАЗДАТАКЛИЕНТПРОДУКТ / OFFERСУММАОПЛАТАИСПОЛНЕНИЕИСТОЧНИКПРОДАВЕЦ +
{row.id}{row.date}{row.client}{row.offer}{row.amount}{row.source.label}{row.seller}
+ ); +} diff --git a/apps/internal-ui/src/features/sales/orders/tables/PaymentsTable.tsx b/apps/internal-ui/src/features/sales/orders/tables/PaymentsTable.tsx new file mode 100644 index 0000000..32efeb0 --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/tables/PaymentsTable.tsx @@ -0,0 +1,29 @@ +import { MonoLink, StatusBadge } from "../StatusBadge"; +import type { SalesPayment } from "../types"; + +export function PaymentsTable({ rows }: { rows: SalesPayment[] }) { + if (rows.length === 0) return null; + return ( + + + + + + + + {rows.map((row) => ( + + + + + + + + + + + ))} + +
ПЛАТЁЖЗАКАЗПРОВАЙДЕРСУММАСТАТУСМЕТОДДАТАСВЕРКА
{row.id}{row.order}{row.provider}{row.amount}{row.method}{row.date}{row.reconcile.label}
+ ); +} diff --git a/apps/internal-ui/src/features/sales/orders/tables/RefundsTable.tsx b/apps/internal-ui/src/features/sales/orders/tables/RefundsTable.tsx new file mode 100644 index 0000000..359076c --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/tables/RefundsTable.tsx @@ -0,0 +1,28 @@ +import { StatusBadge } from "../StatusBadge"; +import type { SalesRefund } from "../types"; + +export function RefundsTable({ rows }: { rows: SalesRefund[] }) { + if (rows.length === 0) return null; + return ( + + + + + + + + {rows.map((row) => ( + + + + + + + + + + ))} + +
ВОЗВРАТЗАКАЗ / ПЛАТЁЖСУММАПРИЧИНАСТАТУСИНИЦИАТОРДАТА
{row.id}{row.reference}{row.amount}{row.reason}{row.actor}{row.date}
+ ); +} diff --git a/apps/internal-ui/src/features/sales/orders/tables/SubscriptionsTable.tsx b/apps/internal-ui/src/features/sales/orders/tables/SubscriptionsTable.tsx new file mode 100644 index 0000000..be432e7 --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/tables/SubscriptionsTable.tsx @@ -0,0 +1,30 @@ +import { Icon } from "../../../../shared/icons"; +import { MonoLink, StatusBadge } from "../StatusBadge"; +import type { SalesSubscription } from "../types"; + +export function SubscriptionsTable({ rows }: { rows: SalesSubscription[] }) { + if (rows.length === 0) return null; + return ( + + + + + + + {rows.map((row) => ( + + + + + + + + + + + ))} + +
ПОДПИСКАКЛИЕНТOFFERСТАТУСПЕРИОДСЛЕД. СПИСАНИЕENTITLEMENT +
{row.id}{row.client}{row.offer}{row.period}{row.next}{row.entitlement}
+ ); +} diff --git a/apps/internal-ui/src/features/sales/orders/types.ts b/apps/internal-ui/src/features/sales/orders/types.ts new file mode 100644 index 0000000..e98d61c --- /dev/null +++ b/apps/internal-ui/src/features/sales/orders/types.ts @@ -0,0 +1,69 @@ +export type SalesOrdersTab = "orders" | "subs" | "pays" | "refunds" | "exec"; + +export type StatusTone = "green" | "amber" | "red" | "blue" | "gray" | "purple"; + +export type StatusBadge = { + color: string; + bg: string; + label: string; +}; + +export type SalesOrder = { + id: string; + date: string; + client: string; + offer: string; + amount: string; + pay: StatusBadge; + fulfillment: StatusBadge; + source: { label: string; color: string }; + seller: string; + sellerAI?: boolean; + search: string; +}; + +export type SalesSubscription = { + id: string; + client: string; + offer: string; + status: StatusBadge; + period: string; + next: string; + entitlement: string; + search: string; +}; + +export type SalesPayment = { + id: string; + order: string; + provider: string; + amount: string; + status: StatusBadge; + method: string; + date: string; + reconcile: { color: string; label: string }; + search: string; +}; + +export type SalesRefund = { + id: string; + reference: string; + amount: string; + reason: string; + status: StatusBadge; + actor: string; + date: string; + search: string; +}; + +export type SalesFulfillment = { + order: string; + product: string; + operation: string; + status: StatusBadge; + attempts: string; + error: string; + updated: string; + canRetry: boolean; + search: string; +}; diff --git a/apps/internal-ui/src/layout/SalesSidebar.tsx b/apps/internal-ui/src/layout/SalesSidebar.tsx index edaf650..833a210 100644 --- a/apps/internal-ui/src/layout/SalesSidebar.tsx +++ b/apps/internal-ui/src/layout/SalesSidebar.tsx @@ -7,7 +7,7 @@ export function SalesSidebar({ route, user, setRoute }: { route: RouteKey; user: { key: "salesOverview" as const, label: "Обзор", icon: "grid" as const }, { key: "salesDialogs" as const, label: "Диалоги", icon: "message" as const, badge: "2" }, { key: "salesClients" as const, label: "Клиенты", icon: "team" as const }, - { label: "Продажи", icon: "cart" as const }, + { key: "salesOrders" as const, label: "Продажи", icon: "cart" as const }, ]; return (