This commit is contained in:
Admin 2025-08-14 14:55:45 +07:00
parent d12c6fbbfc
commit 3a9a4d092d
8 changed files with 147 additions and 67 deletions

File diff suppressed because one or more lines are too long

View File

@ -233,28 +233,6 @@ const clickPublist = async () => {
thiefService.clickByPoint(btn);
};
function extractListings(productsEl: HTMLElement) {
const children = Array.from(productsEl.children);
return children.map((child) => {
// Lấy title (thường là span hoặc div có dir="auto")
const titleEl = child.querySelector('span[dir="auto"], div[dir="auto"]');
const title = titleEl?.textContent?.trim() || "";
// Lấy giá (span có attribute dir="auto")
const priceEl = Array.from(child.querySelectorAll('span[dir="auto"]')).find(
(el) =>
/\d/.test(el.textContent || "") && /[AU$]/.test(el.textContent || "")
);
// Tách lấy số, ví dụ: "AU$20" -> "20"
const priceMatch = priceEl?.textContent?.match(/\d+(?:\.\d+)?/);
const price = priceMatch ? parseFloat(priceMatch[0]) : 0;
return { title, price, el: productsEl };
});
}
/**
* B1. Upload images
* B2. Write title
@ -333,19 +311,79 @@ const handle = async (item: IItem) => {
return true;
};
// function extractListings(productsEl: HTMLElement) {
// const children = Array.from(productsEl.children);
// return children.map((child) => {
// // Lấy title (thường là span hoặc div có dir="auto")
// const titleEl = child.querySelector('span[dir="auto"], div[dir="auto"]');
// const title = titleEl?.textContent?.trim() || "";
// // Lấy giá (span có attribute dir="auto")
// const priceEl = Array.from(child.querySelectorAll('span[dir="auto"]')).find(
// (el) =>
// /\d/.test(el.textContent || "") && /[AU$]/.test(el.textContent || "")
// );
// // Tách lấy số, ví dụ: "AU$20" -> "20"
// const priceMatch = priceEl?.textContent?.match(/\d+(?:\.\d+)?/);
// const price = priceMatch ? parseFloat(priceMatch[0]) : 0;
// return { title, price, el: productsEl };
// });
// }
// const getProducts = async () => {
// let products = await thiefService.getElementByXPath(selectors.products);
// if (!products) {
// products = await thiefService.getElementByXPath(
// selectors.products_fallback
// );
// }
// if (!products) return [];
// return extractListings(products) as ISyncItem[];
// };
const getProducts = async () => {
let products = await thiefService.getElementByXPath(selectors.products);
if (!products) {
products = await thiefService.getElementByXPath(
selectors.products_fallback
);
}
const products1 = await thiefService.getElementByXPath(selectors.products);
const products2 = await thiefService.getElementByXPath(
selectors.products_fallback
);
if (!products) return [];
// Gom 2 cái vào một mảng, bỏ null
const allProductsEls = [products1, products2].filter(
Boolean
) as HTMLElement[];
return extractListings(products) as ISyncItem[];
if (allProductsEls.length === 0) return [];
// Nối tất cả kết quả extractListings từ mỗi element
return allProductsEls.flatMap((el) => extractListings(el)) as ISyncItem[];
};
function extractListings(productsEl: HTMLElement) {
const children = Array.from(productsEl.children);
return children.map((child) => {
// Lấy title
const titleEl = child.querySelector('span[dir="auto"], div[dir="auto"]');
const title = titleEl?.textContent?.trim() || "";
// Lấy giá
const priceEl = Array.from(child.querySelectorAll('span[dir="auto"]')).find(
(el) =>
/\d/.test(el.textContent || "") && /[AU$]/.test(el.textContent || "")
);
const priceMatch = priceEl?.textContent?.match(/\d+(?:\.\d+)?/);
const price = priceMatch ? parseFloat(priceMatch[0]) : 0;
return { title, price, el: productsEl };
});
}
const syncListing = async () => {
const url = window.location.href;
if (!url.includes("https://www.facebook.com/marketplace/you/selling")) return;

View File

@ -158,7 +158,7 @@ export interface Column<T> {
// Thêm interface cho custom actions
interface CustomAction<T> {
key: string;
label: string;
label: string | ((row: T) => ReactNode);
icon?: React.ReactNode;
variant?: "default" | "secondary" | "destructive" | "outline";
action: (row: T) => void;
@ -1470,7 +1470,9 @@ export function DataTable<T extends Record<string, any>>({
}`}
>
{action.icon && <span className="mr-2">{action.icon}</span>}
{action.label}
{typeof action.label === "string"
? action.label
: action.label(row)}
</DropdownMenuItem>
))}

View File

@ -4,7 +4,7 @@ import {
MoreHorizontal,
Trash2,
type LucideIcon,
} from "lucide-react"
} from "lucide-react";
import {
DropdownMenu,
@ -12,7 +12,7 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"
} from "~/components/ui/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
@ -21,18 +21,18 @@ import {
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "~/components/ui/sidebar"
} from "~/components/ui/sidebar";
export function NavProjects({
projects,
}: {
projects: {
name: string
url: string
icon: LucideIcon
}[]
name: string;
url: string;
icon: LucideIcon;
}[];
}) {
const { isMobile } = useSidebar()
const { isMobile } = useSidebar();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
@ -83,5 +83,5 @@ export function NavProjects({
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
);
}

View File

@ -1,3 +1,4 @@
import { useEffect } from "react";
import { Outlet } from "react-router";
import { AppSidebar } from "~/components/app-sidebar";
@ -14,6 +15,7 @@ import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
useSidebar,
} from "~/components/ui/sidebar";
export default function PrivateLayout() {
@ -45,8 +47,9 @@ export default function PrivateLayout() {
// <Loader showLabel={true} size="size-10" />
// </div>
// );
return (
<SidebarProvider>
<SidebarProvider open={false}>
<AppSidebar />
<SidebarInset className="h-screen overflow-x-hidden">
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 sticky top-0 bg-white z-50">

View File

@ -208,8 +208,10 @@ export default function List() {
// 🎯 Custom Actions - Đa dạng và có điều kiện
const customActions = [
{
key: "publist",
label: "Publist",
key: "listing",
label: (row: IProduct) => {
return row?.status ? "Unlist" : "Publist";
},
icon: <UploadCloud className="h-4 w-4" />,
action: (data: IProduct) => {
handlePublist(data);
@ -259,11 +261,15 @@ export default function List() {
enabled: !!states,
});
const publistMutation = useMutation({
const listingMutation = useMutation({
mutationFn: async (data: Partial<IProduct>) => {
await delay(300);
return productApi.customAction(data.id || 0, "publist", data);
if (data.status) {
return productApi.customAction(data.id || 0, "unlist", data);
} else {
return productApi.customAction(data.id || 0, "publist", data);
}
},
onSuccess: (data) => {
console.log({ data });
@ -273,7 +279,7 @@ export default function List() {
const handlePublist = async (data: Partial<IProduct>) => {
toast.promise(
publistMutation.mutateAsync(data), // gọi function để trả về promise
listingMutation.mutateAsync(data), // gọi function để trả về promise
{
loading: "Loading...",
success: (result) => `${data?.title} toast has been added`,

View File

@ -30,6 +30,11 @@ export class ProductsController extends CoreController<
return this.service.publist(id);
}
@Post('unlist/:id')
async unlist(@Param('id') id: Product['id']): Promise<any> {
return this.service.unlist(id);
}
@Post('publist-finish/:id')
async publistFinish(
@Param('id') id: Product['id'],

View File

@ -74,28 +74,28 @@ export class ProductsService extends CoreService<Product> {
}
}
protected async beforeDelete(
id: number,
data: Partial<Product>,
req: Request,
): Promise<boolean> {
// Emit sự kiện để bắt đầu publish
this.eventService.sendEvent(ProductsService.EVENTS.SEND_DELETE, {
...data,
});
// protected async beforeDelete(
// id: number,
// data: Partial<Product>,
// req: Request,
// ): Promise<boolean> {
// // Emit sự kiện để bắt đầu publish
// this.eventService.sendEvent(ProductsService.EVENTS.SEND_DELETE, {
// ...data,
// });
// Đợi phản hồi từ client
const result = await this.waitForDeleteResult({ ...data, id });
// // Đợi phản hồi từ client
// const result = await this.waitForDeleteResult({ ...data, id });
if (!result)
throw new BadRequestException(
AppResponse.toResponse(false, {
message: SystemLang.getText('messages', 'try_again'),
}),
);
// if (!result)
// throw new BadRequestException(
// AppResponse.toResponse(false, {
// message: SystemLang.getText('messages', 'try_again'),
// }),
// );
return true;
}
// return true;
// }
async create(data: CreateProductDto): Promise<any> {
const product = this.repo.create(data);
@ -197,6 +197,32 @@ export class ProductsService extends CoreService<Product> {
return AppResponse.toResponse(plainData);
}
async unlist(id: Product['id']) {
const product = await this.repo.findOne({ where: { id } });
if (!product) {
throw new NotFoundException(
AppResponse.toResponse(null, {
message: SystemLang.getText('messages', 'not_found'),
}),
);
}
const plainData = plainToClass(Product, product);
// Emit sự kiện để bắt đầu publish
this.eventService.sendEvent(ProductsService.EVENTS.SEND_DELETE, {
...plainData,
});
// Đợi phản hồi từ client
const publistResult = await this.waitForDeleteResult(plainData);
await this.repo.update(plainData.id, { status: false });
return AppResponse.toResponse(plainData);
}
/**
* Chờ event PUBLIST_FINISH bắt lỗi nếu quá thời gian hoặc lỗi bất ngờ
*/