diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx index 80a0f64..a067ad3 100644 --- a/src/app/account/page.tsx +++ b/src/app/account/page.tsx @@ -22,6 +22,7 @@ import { import AccountProfile from "../../components/AccountProfile"; import AccountSecurity from "../../components/AccountSecurity"; import AccountNotifications from "../../components/AccountNotifications"; +import AccountAgentTransactionSection from "../../components/AccountAgentTransactionSection"; const initialNotifications = { emailNotifications: true, @@ -41,12 +42,11 @@ export default function AccountPage() { }); const [notifications, setNotifications] = useState(initialNotifications); - - const tabs = [ { id: "profile", label: "Профиль", icon: }, { id: "security", label: "Безопасность", icon: }, { id: "notifications", label: "Уведомления", icon: }, + { id: "agent-transactions", label: "Транзакции агентов", icon: }, ]; return ( @@ -80,6 +80,9 @@ export default function AccountPage() { {activeTab === "notifications" && ( )} + {activeTab === "agent-transactions" && ( + + )} ); diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index eb8a895..e992f30 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -40,10 +40,10 @@ export default function AuthPage() { const res = await fetch("/api/token", { method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/x-www-form-urlencoded", }, - body: JSON.stringify({ - login: email, + body: new URLSearchParams({ + username: email, password: password, }), }); diff --git a/src/components/AccountAgentTransactionSection.tsx b/src/components/AccountAgentTransactionSection.tsx new file mode 100644 index 0000000..795ec27 --- /dev/null +++ b/src/components/AccountAgentTransactionSection.tsx @@ -0,0 +1,122 @@ +import { useState } from "react"; +import DateFilters from "./DateFilters"; +import AccountAgentTransactionTable from "./AccountAgentTransactionTable"; +import styles from "../styles/account.module.css"; + +export default function AccountAgentTransactionSection() { + const [filters, setFilters] = useState({ + dateStart: "", + dateEnd: "", + }); + const [reloadKey, setReloadKey] = useState(0); + const [autoConfirmEnabled, setAutoConfirmEnabled] = useState(false); + const [showConfirmationUI, setShowConfirmationUI] = useState(false); + + function handleApply() { + setReloadKey(k => k + 1); + } + function handleClear() { + setFilters(f => ({ ...f, dateStart: '', dateEnd: '' })); + setReloadKey(k => k + 1); + } + + const handleToggleChange = (e: React.ChangeEvent) => { + const isChecked = e.target.checked; + if (isChecked) { + setShowConfirmationUI(true); + } else { + setAutoConfirmEnabled(false); + setShowConfirmationUI(false); + console.log("Автоматическое подтверждение выключено."); + } + }; + + const handleConfirmYes = () => { + setAutoConfirmEnabled(true); + setShowConfirmationUI(false); + console.log("Автоматическое подтверждение включено и текущие транзакции будут подтверждены."); + // TODO: Добавить логику автоматического подтверждения текущих транзакций + }; + + const handleConfirmNo = () => { + setAutoConfirmEnabled(true); + setShowConfirmationUI(false); + console.log("Автоматическое подтверждение включено для новых транзакций. Текущие остаются неподтвержденными."); + // Текущие транзакции остаются в статусе waiting, ничего дополнительно делать не нужно + }; + + const handleConfirmCancel = () => { + setAutoConfirmEnabled(false); + setShowConfirmationUI(false); + console.log("Включение автоматического подтверждения отменено."); + }; + + return ( + <> +
+
+

Настройки выплат

+
+
+
+
Автоматическое подтверждение
+
+ +
+ + {showConfirmationUI && ( +
+

Подтвердить автоматически текущие транзакции?

+
+ + + +
+
+ )} + +
+
+
+ + { + if (field === "dateStart") { + if (filters.dateEnd && value > filters.dateEnd) return; + } + if (field === "dateEnd") { + if (filters.dateStart && value < filters.dateStart) return; + } + setFilters(f => ({ ...f, [field]: value })); + }} + onApply={handleApply} + onClear={handleClear} + /> + + + ); +} \ No newline at end of file diff --git a/src/components/AccountAgentTransactionTable.tsx b/src/components/AccountAgentTransactionTable.tsx new file mode 100644 index 0000000..1268d99 --- /dev/null +++ b/src/components/AccountAgentTransactionTable.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState, useMemo } from 'react'; +import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from 'material-react-table'; +import styles from "../styles/stat.module.css"; +import { Box, Button } from '@mui/material'; +import FileDownloadIcon from '@mui/icons-material/FileDownload'; +import { mkConfig, generateCsv, download } from 'export-to-csv'; +import Cookies from 'js-cookie'; +import PaymentIcon from '@mui/icons-material/Payment'; + +const STATUS_COLORS: Record = { + "done": "#10B981", // Зеленый + "waiting": "#F59E0B", // Желтый + "error": "#EF4444", // Красный + "process": "#3B82F6", // Синий + "reject": "#800080", // Фиолетовый +}; + +function formatCurrency(amount: number) { + return amount?.toLocaleString("ru-RU", { + style: "currency", + currency: "RUB", + minimumFractionDigits: 0, + }) ?? ""; +} + +export default function AccountAgentTransactionTable({ filters, reloadKey }: { filters: { dateStart: string, dateEnd: string }, reloadKey: number }) { + const [data, setData] = useState([]); + + useEffect(() => { + const token = Cookies.get("access_token"); + + if (token) { + const params = new URLSearchParams(); + if (filters.dateStart) params.append('date_start', filters.dateStart); + if (filters.dateEnd) params.append('date_end', filters.dateEnd); + + params.append('statuses', 'waiting'); + + fetch(`/api/account/agent-transaction?${params.toString()}`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => { + if (!res.ok) { + console.error('Ошибка при загрузке данных транзакций агентов:', res.status, res.statusText); + setData([]); + return null; + } + return res.json(); + }) + .then(data => { + if (data !== null) { + setData(data); + } + }) + .catch(error => { + console.error('Ошибка при загрузке данных транзакций агентов:', error); + setData([]); + }); + } else { + console.warn('Токен авторизации не найден в куках.'); + setData([]); + } + + }, [filters.dateStart, filters.dateEnd, reloadKey]); + + const columns = useMemo[]>( + () => [ + { accessorKey: 'transaction_group', header: 'ID Транзакции' }, + { accessorKey: 'amount', header: 'Сумма', Cell: ({ cell }) => formatCurrency(cell.getValue() as number) }, + { accessorKey: 'agent_name', header: 'Агент' }, + { accessorKey: 'status', header: 'Статус', + Cell: ({ cell }) => { + const status = cell.getValue() as string; + const color = STATUS_COLORS[status] || '#A3A3A3'; + return {status}; + } + }, + { accessorKey: 'create_dttm', header: 'Дата создания', Cell: ({ cell }) => new Date(cell.getValue() as string).toLocaleDateString() }, + { accessorKey: 'update_dttm', header: 'Дата обновления', Cell: ({ cell }) => new Date(cell.getValue() as string).toLocaleDateString() }, + ], + [] + ); + + const csvConfig = mkConfig({ + fieldSeparator: ',', + decimalSeparator: '.', + useKeysAsHeaders: true, + }); + + const table = useMaterialReactTable({ + columns, + data, + enableRowSelection: true, + renderTopToolbarCustomActions: ({ table }) => ( + + + + + + + ), + muiTableBodyCellProps: { sx: { fontSize: 14 } }, + muiTableHeadCellProps: { sx: { fontWeight: 700 } }, + initialState: { pagination: { pageSize: 10, pageIndex: 0 } }, + }); + + const handleExportRows = (rows: any[]) => { + const rowData = rows.map((row) => row.original); + const csv = generateCsv(csvConfig)(rowData); + download(csvConfig)(csv); + }; + + const handleExportData = () => { + const csv = generateCsv(csvConfig)(data); + download(csvConfig)(csv); + }; + + return ; +} \ No newline at end of file diff --git a/src/components/BillingPieChart.tsx b/src/components/BillingPieChart.tsx index c40ffe8..af92b8d 100644 --- a/src/components/BillingPieChart.tsx +++ b/src/components/BillingPieChart.tsx @@ -8,12 +8,13 @@ const STATUS_COLORS: Record = { "waiting": "#F59E0B", "error": "#EF4444", "process": "#3B82F6", + "reject": "#800080", }; const BillingPieChart: React.FC = () => { const [data, setData] = useState<{ name: string; value: number; fill: string }[]>([]); useEffect(() => { - fetch("api/billing/chart/pie") + fetch("/api/billing/chart/pie") .then((res) => res.json()) .then((apiData) => { const mapped = apiData.map((item: { status: string; count: number }) => ({ diff --git a/src/components/BillingStatChart.tsx b/src/components/BillingStatChart.tsx index 88fa916..fc72c47 100644 --- a/src/components/BillingStatChart.tsx +++ b/src/components/BillingStatChart.tsx @@ -10,6 +10,7 @@ const statusColors: Record = { process: "#3B82F6", waiting: "#F59E42", error: "#EF4444", + reject: "#800080", }; const BillingStatChart: React.FC = () => { diff --git a/src/components/PayoutsTransactionsTable.tsx b/src/components/PayoutsTransactionsTable.tsx index a7daa84..254634f 100644 --- a/src/components/PayoutsTransactionsTable.tsx +++ b/src/components/PayoutsTransactionsTable.tsx @@ -19,6 +19,7 @@ const statusColor: Record = { 'waiting': '#ff9800', 'process': '#2196f3', 'error': '#f44336', + 'reject': '#800080', }; export default function PayoutsTransactionsTable({ filters, reloadKey }: { filters: { dateStart: string, dateEnd: string }, reloadKey: number }) { @@ -36,7 +37,7 @@ export default function PayoutsTransactionsTable({ filters, reloadKey }: { filte const columns = useMemo[]>( () => [ { accessorKey: 'id', header: 'ID' }, - { accessorKey: 'sum', header: 'Сумма', + { accessorKey: 'amount', header: 'Сумма', Cell: ({ cell }) => formatCurrency(cell.getValue() as number) }, { accessorKey: 'agent', header: 'Агент' }, { accessorKey: 'status', header: 'Статус', diff --git a/src/styles/account.module.css b/src/styles/account.module.css index 3f29cec..e820b83 100644 --- a/src/styles/account.module.css +++ b/src/styles/account.module.css @@ -391,4 +391,49 @@ font-weight: 600; } +/* Стили для кнопок подтверждения */ +.primaryButton { + background-color: #2563eb; /* Синий */ + color: #ffffff; + border: none; + border-radius: 6px; + padding: 8px 16px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.primaryButton:hover { + background-color: #1d4ed8; +} + +.secondaryButton { + background-color: #f3f4f6; /* Серый */ + color: #374151; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 8px 16px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.secondaryButton:hover { + background-color: #e5e7eb; +} + +.tertiaryButton { + background: none; + color: #6b7280; /* Темно-серый */ + border: none; + padding: 8px 16px; + font-weight: 500; + cursor: pointer; + transition: color 0.2s ease; +} + +.tertiaryButton:hover { + color: #4b5563; +} + /* ... можно добавить другие стили по необходимости ... */ \ No newline at end of file