creat action and ci cd
CI/CD Pipeline / build-and-deploy (push) Has been cancelled Details

This commit is contained in:
vahidrezvani 2026-02-22 08:50:11 +03:30
parent 16cd65c4da
commit 3e227089fa
4 changed files with 155 additions and 62 deletions

51
.github/workflows/deploy.yml vendored Normal file
View File

@ -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

View File

@ -9,91 +9,110 @@ import { Receipt, SearchNormal1 } from "iconsax-react";
import { API_ROUTES, request } from "@/services/api";
import { Container } from "@/components/container";
import type { Transaction } from "@/types/transaction";
import type { Transaction, TransactionListResponse } from "@/types/transaction";
const { Option } = Select;
export default function TransactionsPage() {
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
// filters
const [search, setSearch] = useState("");
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, {
page,
limit: 10,
q: search,
type: typeFilter,
size: 10,
...(search ? { q: search } : {}),
...(typeFilter ? { type: typeFilter } : {}),
...(statusFilter ? { status: statusFilter } : {}),
});
setTotal(res.data.total_count || 0);
return res.data.transactions as Transaction[];
return res.data as TransactionListResponse;
};
const { data, isLoading } = useQuery({
queryKey: ["transactions", page, search, typeFilter],
queryKey: ["transactions", page, search, typeFilter, statusFilter],
queryFn: fetchData,
});
const columns: ColumnsType<Transaction> = [
{
title: "#",
dataIndex: "id",
key: "id",
width: 60,
render: (id: number) => <span className="text-gray-400 text-xs">{id}</span>,
},
{
title: "کاربر",
dataIndex: "user",
key: "user",
render: (user) =>
user ? (
render: (user: Transaction["user"]) => (
<div className="flex flex-col gap-0.5">
<Link
href={`/user/${user.username}`}
className="text-blue-600 font-medium hover:underline"
href={`/user/${user.id}`}
className="text-blue-600 font-medium hover:underline text-sm"
>
{user.username}
{user.name}
</Link>
) : (
<span className="text-gray-400">سیستمی / حذف شده</span>
),
<span className="text-gray-400 text-xs" dir="ltr">{user.mobile_number}</span>
</div>
),
},
{
title: "مقدار",
title: "اعتبار",
dataIndex: "credit",
key: "credit",
render: (credit: number) => (
<span className="font-semibold text-indigo-600">{credit.toLocaleString()}</span>
),
},
{
title: "مبلغ (ریال)",
dataIndex: "amount",
key: "amount",
render: (amount: number) => (
<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: "نوع",
dataIndex: "type",
key: "type",
render: (type: string) => {
let color: "default" | "green" | "blue" | "gold" = "default";
let text = type;
switch (type) {
case "gift":
color = "green";
text = "هدیه";
break;
case "credit":
color = "blue";
text = "اعتبار";
break;
case "free":
color = "gold";
text = "رایگان";
break;
}
return <Tag color={color}>{text}</Tag>;
const map: Record<string, { color: string; label: string }> = {
basa: { color: "purple", label: "باسا" },
gift: { color: "green", label: "هدیه" },
credit: { color: "blue", label: "اعتبار" },
free: { color: "gold", label: "رایگان" },
};
const info = map[type] ?? { color: "default", label: type };
return <Tag color={info.color}>{info.label}</Tag>;
},
},
{
title: "توضیحات",
dataIndex: "description",
key: "description",
ellipsis: true,
title: "وضعیت",
dataIndex: "status",
key: "status",
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: "تاریخ",
@ -127,7 +146,7 @@ export default function TransactionsPage() {
<div className="flex flex-col md:flex-row gap-4">
<Input
prefix={<SearchNormal1 size={18} className="text-gray-400" />}
placeholder="جستجو (نام کاربری، توضیحات...)"
placeholder="جستجو (نام، شماره موبایل، شماره پیگیری...)"
className="md:w-1/3"
allowClear
value={search}
@ -137,22 +156,36 @@ export default function TransactionsPage() {
<Select
placeholder="نوع تراکنش"
allowClear
className="md:w-48"
className="md:w-40"
value={typeFilter}
onChange={(value) => setTypeFilter(value)}
onChange={(value) => { setTypeFilter(value); setPage(1); }}
>
<Option value="gift">هدیه (Gift)</Option>
<Option value="credit">اعتبار (Credit)</Option>
<Option value="free">رایگان (Free)</Option>
<Option value="basa">باسا</Option>
<Option value="gift">هدیه</Option>
<Option value="credit">اعتبار</Option>
<Option value="free">رایگان</Option>
</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
type="text"
danger
onClick={() => {
setSearch("");
setTypeFilter(undefined);
setStatusFilter(undefined);
setPage(1);
}}
>
@ -165,16 +198,17 @@ export default function TransactionsPage() {
{/* Table */}
<Table
columns={columns}
dataSource={data}
dataSource={data?.items}
rowKey="id"
loading={isLoading}
pagination={{
current: page,
pageSize: 10,
total,
pageSize: data?.size ?? 10,
total: data?.total ?? 0,
onChange: setPage,
showSizeChanger: false,
position: ["bottomCenter"],
showTotal: (total) => `مجموع ${total} تراکنش`,
}}
/>
</div>

View File

@ -13,7 +13,7 @@ const API_ROUTES = {
ONE: (id: string) => `/admin/ticket/${id}`,
},
TRANSACTION: {
ALL: "/admin/transaction/", // اندپوینت دریافت همه تراکنش‌ها
ALL: "/admin/payment/", // اندپوینت دریافت همه تراکنش‌ها
},
USER: {
ALL: "/admin/user/",

View File

@ -1,17 +1,25 @@
// panel-houshan/src/types/transaction.ts
export type TransactionType = "gift" | "credit" | "free";
export type TransactionStatus = "success" | "failed" | "pending" | string;
export type Transaction = {
id: string;
id: number;
credit: number;
amount: number;
type: TransactionType;
description: string;
ref_id: string;
type: string;
status: TransactionStatus;
created_at: string;
// این فیلد برای نمایش نام کاربر در لیست کلی اضافه می‌شود
user?: {
user: {
id: string;
name: string;
mobile_number: string;
username: string;
name?: string;
};
};
export type TransactionListResponse = {
total: number;
page: number;
size: number;
total_pages: number;
items: Transaction[];
};