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

View File

@ -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/",

View File

@ -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[];
};