creat action and ci cd
CI/CD Pipeline / build-and-deploy (push) Has been cancelled
Details
CI/CD Pipeline / build-and-deploy (push) Has been cancelled
Details
This commit is contained in:
parent
16cd65c4da
commit
3e227089fa
|
|
@ -0,0 +1,51 @@
|
||||||
|
name: CI/CD Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main # اگر اسم برنچ اصلیت master هست، این رو بکن master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker Image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
didvan/basa_houshan_panel:latest
|
||||||
|
didvan/basa_houshan_panel:${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Deploy to Server
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SERVER_IP_BASA_PANEL }}
|
||||||
|
username: ${{ secrets.SERVER_USERNAME }}
|
||||||
|
# اگر از کلید SSH استفاده میکنی این خط بمونه:
|
||||||
|
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||||
|
# اگر از پسورد استفاده میکنی، خط بالا رو پاک کن و خط پایین رو از کامنت دربیار:
|
||||||
|
# password: ${{ secrets.SERVER_PASSWORD }}
|
||||||
|
script: |
|
||||||
|
# !!! بسیار مهم: مسیر دقیق پوشه پروژه روی سرورت رو اینجا جایگزین کن !!!
|
||||||
|
cd /home/ubuntu/app/panel-houshan
|
||||||
|
|
||||||
|
# لاگین به داکر هاب روی سرور (برای دانلود ایمیجهای Private)
|
||||||
|
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
|
# دانلود ایمیج جدید و ریاستارت فقط همون کانتینر
|
||||||
|
docker compose pull app
|
||||||
|
docker compose up -d app
|
||||||
|
|
||||||
|
# پاکسازی ایمیجهای قدیمی
|
||||||
|
docker image prune -f
|
||||||
|
|
@ -9,91 +9,110 @@ import { Receipt, SearchNormal1 } from "iconsax-react";
|
||||||
|
|
||||||
import { API_ROUTES, request } from "@/services/api";
|
import { API_ROUTES, request } from "@/services/api";
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import type { Transaction } from "@/types/transaction";
|
import type { Transaction, TransactionListResponse } from "@/types/transaction";
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
export default function TransactionsPage() {
|
export default function TransactionsPage() {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [typeFilter, setTypeFilter] = useState<string | undefined>(undefined);
|
const [typeFilter, setTypeFilter] = useState<string | undefined>(undefined);
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async (): Promise<TransactionListResponse> => {
|
||||||
const res = await request.get(API_ROUTES.TRANSACTION.ALL, {
|
const res = await request.get(API_ROUTES.TRANSACTION.ALL, {
|
||||||
page,
|
page,
|
||||||
limit: 10,
|
size: 10,
|
||||||
q: search,
|
...(search ? { q: search } : {}),
|
||||||
type: typeFilter,
|
...(typeFilter ? { type: typeFilter } : {}),
|
||||||
|
...(statusFilter ? { status: statusFilter } : {}),
|
||||||
});
|
});
|
||||||
|
return res.data as TransactionListResponse;
|
||||||
setTotal(res.data.total_count || 0);
|
|
||||||
return res.data.transactions as Transaction[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ["transactions", page, search, typeFilter],
|
queryKey: ["transactions", page, search, typeFilter, statusFilter],
|
||||||
queryFn: fetchData,
|
queryFn: fetchData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns: ColumnsType<Transaction> = [
|
const columns: ColumnsType<Transaction> = [
|
||||||
|
{
|
||||||
|
title: "#",
|
||||||
|
dataIndex: "id",
|
||||||
|
key: "id",
|
||||||
|
width: 60,
|
||||||
|
render: (id: number) => <span className="text-gray-400 text-xs">{id}</span>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "کاربر",
|
title: "کاربر",
|
||||||
dataIndex: "user",
|
dataIndex: "user",
|
||||||
key: "user",
|
key: "user",
|
||||||
render: (user) =>
|
render: (user: Transaction["user"]) => (
|
||||||
user ? (
|
<div className="flex flex-col gap-0.5">
|
||||||
<Link
|
<Link
|
||||||
href={`/user/${user.username}`}
|
href={`/user/${user.id}`}
|
||||||
className="text-blue-600 font-medium hover:underline"
|
className="text-blue-600 font-medium hover:underline text-sm"
|
||||||
>
|
>
|
||||||
{user.username}
|
{user.name}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
<span className="text-gray-400 text-xs" dir="ltr">{user.mobile_number}</span>
|
||||||
<span className="text-gray-400">سیستمی / حذف شده</span>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "مقدار",
|
title: "اعتبار",
|
||||||
|
dataIndex: "credit",
|
||||||
|
key: "credit",
|
||||||
|
render: (credit: number) => (
|
||||||
|
<span className="font-semibold text-indigo-600">{credit.toLocaleString()}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "مبلغ (ریال)",
|
||||||
dataIndex: "amount",
|
dataIndex: "amount",
|
||||||
key: "amount",
|
key: "amount",
|
||||||
render: (amount: number) => (
|
render: (amount: number) => (
|
||||||
<span className="font-bold">{amount.toLocaleString()}</span>
|
<span className="font-bold">{amount.toLocaleString()}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "شماره پیگیری",
|
||||||
|
dataIndex: "ref_id",
|
||||||
|
key: "ref_id",
|
||||||
|
render: (ref_id: string) => (
|
||||||
|
<span dir="ltr" className="text-xs font-mono text-gray-600">{ref_id || "—"}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "نوع",
|
title: "نوع",
|
||||||
dataIndex: "type",
|
dataIndex: "type",
|
||||||
key: "type",
|
key: "type",
|
||||||
render: (type: string) => {
|
render: (type: string) => {
|
||||||
let color: "default" | "green" | "blue" | "gold" = "default";
|
const map: Record<string, { color: string; label: string }> = {
|
||||||
let text = type;
|
basa: { color: "purple", label: "باسا" },
|
||||||
|
gift: { color: "green", label: "هدیه" },
|
||||||
switch (type) {
|
credit: { color: "blue", label: "اعتبار" },
|
||||||
case "gift":
|
free: { color: "gold", label: "رایگان" },
|
||||||
color = "green";
|
};
|
||||||
text = "هدیه";
|
const info = map[type] ?? { color: "default", label: type };
|
||||||
break;
|
return <Tag color={info.color}>{info.label}</Tag>;
|
||||||
case "credit":
|
|
||||||
color = "blue";
|
|
||||||
text = "اعتبار";
|
|
||||||
break;
|
|
||||||
case "free":
|
|
||||||
color = "gold";
|
|
||||||
text = "رایگان";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Tag color={color}>{text}</Tag>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "توضیحات",
|
title: "وضعیت",
|
||||||
dataIndex: "description",
|
dataIndex: "status",
|
||||||
key: "description",
|
key: "status",
|
||||||
ellipsis: true,
|
render: (status: string) => {
|
||||||
|
const map: Record<string, { color: string; label: string }> = {
|
||||||
|
success: { color: "success", label: "موفق" },
|
||||||
|
failed: { color: "error", label: "ناموفق" },
|
||||||
|
pending: { color: "warning", label: "در انتظار" },
|
||||||
|
};
|
||||||
|
const info = map[status] ?? { color: "default", label: status };
|
||||||
|
return <Tag color={info.color}>{info.label}</Tag>;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "تاریخ",
|
title: "تاریخ",
|
||||||
|
|
@ -127,7 +146,7 @@ export default function TransactionsPage() {
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<Input
|
<Input
|
||||||
prefix={<SearchNormal1 size={18} className="text-gray-400" />}
|
prefix={<SearchNormal1 size={18} className="text-gray-400" />}
|
||||||
placeholder="جستجو (نام کاربری، توضیحات...)"
|
placeholder="جستجو (نام، شماره موبایل، شماره پیگیری...)"
|
||||||
className="md:w-1/3"
|
className="md:w-1/3"
|
||||||
allowClear
|
allowClear
|
||||||
value={search}
|
value={search}
|
||||||
|
|
@ -137,22 +156,36 @@ export default function TransactionsPage() {
|
||||||
<Select
|
<Select
|
||||||
placeholder="نوع تراکنش"
|
placeholder="نوع تراکنش"
|
||||||
allowClear
|
allowClear
|
||||||
className="md:w-48"
|
className="md:w-40"
|
||||||
value={typeFilter}
|
value={typeFilter}
|
||||||
onChange={(value) => setTypeFilter(value)}
|
onChange={(value) => { setTypeFilter(value); setPage(1); }}
|
||||||
>
|
>
|
||||||
<Option value="gift">هدیه (Gift)</Option>
|
<Option value="basa">باسا</Option>
|
||||||
<Option value="credit">اعتبار (Credit)</Option>
|
<Option value="gift">هدیه</Option>
|
||||||
<Option value="free">رایگان (Free)</Option>
|
<Option value="credit">اعتبار</Option>
|
||||||
|
<Option value="free">رایگان</Option>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{(search || typeFilter) && (
|
<Select
|
||||||
|
placeholder="وضعیت"
|
||||||
|
allowClear
|
||||||
|
className="md:w-36"
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(value) => { setStatusFilter(value); setPage(1); }}
|
||||||
|
>
|
||||||
|
<Option value="success">موفق</Option>
|
||||||
|
<Option value="failed">ناموفق</Option>
|
||||||
|
<Option value="pending">در انتظار</Option>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{(search || typeFilter || statusFilter) && (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
danger
|
danger
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearch("");
|
setSearch("");
|
||||||
setTypeFilter(undefined);
|
setTypeFilter(undefined);
|
||||||
|
setStatusFilter(undefined);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -165,16 +198,17 @@ export default function TransactionsPage() {
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={data?.items}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize: 10,
|
pageSize: data?.size ?? 10,
|
||||||
total,
|
total: data?.total ?? 0,
|
||||||
onChange: setPage,
|
onChange: setPage,
|
||||||
showSizeChanger: false,
|
showSizeChanger: false,
|
||||||
position: ["bottomCenter"],
|
position: ["bottomCenter"],
|
||||||
|
showTotal: (total) => `مجموع ${total} تراکنش`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const API_ROUTES = {
|
||||||
ONE: (id: string) => `/admin/ticket/${id}`,
|
ONE: (id: string) => `/admin/ticket/${id}`,
|
||||||
},
|
},
|
||||||
TRANSACTION: {
|
TRANSACTION: {
|
||||||
ALL: "/admin/transaction/", // اندپوینت دریافت همه تراکنشها
|
ALL: "/admin/payment/", // اندپوینت دریافت همه تراکنشها
|
||||||
},
|
},
|
||||||
USER: {
|
USER: {
|
||||||
ALL: "/admin/user/",
|
ALL: "/admin/user/",
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
// panel-houshan/src/types/transaction.ts
|
export type TransactionStatus = "success" | "failed" | "pending" | string;
|
||||||
|
|
||||||
export type TransactionType = "gift" | "credit" | "free";
|
|
||||||
|
|
||||||
export type Transaction = {
|
export type Transaction = {
|
||||||
id: string;
|
id: number;
|
||||||
|
credit: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
type: TransactionType;
|
ref_id: string;
|
||||||
description: string;
|
type: string;
|
||||||
|
status: TransactionStatus;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
// این فیلد برای نمایش نام کاربر در لیست کلی اضافه میشود
|
user: {
|
||||||
user?: {
|
|
||||||
id: string;
|
id: string;
|
||||||
|
name: string;
|
||||||
|
mobile_number: string;
|
||||||
username: string;
|
username: string;
|
||||||
name?: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TransactionListResponse = {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total_pages: number;
|
||||||
|
items: Transaction[];
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue