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); 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 * B1. Upload images
* B2. Write title * B2. Write title
@ -333,19 +311,79 @@ const handle = async (item: IItem) => {
return true; 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 () => { const getProducts = async () => {
let products = await thiefService.getElementByXPath(selectors.products); const products1 = await thiefService.getElementByXPath(selectors.products);
if (!products) { const products2 = await thiefService.getElementByXPath(
products = await thiefService.getElementByXPath( selectors.products_fallback
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 syncListing = async () => {
const url = window.location.href; const url = window.location.href;
if (!url.includes("https://www.facebook.com/marketplace/you/selling")) return; 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 // Thêm interface cho custom actions
interface CustomAction<T> { interface CustomAction<T> {
key: string; key: string;
label: string; label: string | ((row: T) => ReactNode);
icon?: React.ReactNode; icon?: React.ReactNode;
variant?: "default" | "secondary" | "destructive" | "outline"; variant?: "default" | "secondary" | "destructive" | "outline";
action: (row: T) => void; 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.icon && <span className="mr-2">{action.icon}</span>}
{action.label} {typeof action.label === "string"
? action.label
: action.label(row)}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}

View File

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

View File

@ -1,3 +1,4 @@
import { useEffect } from "react";
import { Outlet } from "react-router"; import { Outlet } from "react-router";
import { AppSidebar } from "~/components/app-sidebar"; import { AppSidebar } from "~/components/app-sidebar";
@ -14,6 +15,7 @@ import {
SidebarInset, SidebarInset,
SidebarProvider, SidebarProvider,
SidebarTrigger, SidebarTrigger,
useSidebar,
} from "~/components/ui/sidebar"; } from "~/components/ui/sidebar";
export default function PrivateLayout() { export default function PrivateLayout() {
@ -45,8 +47,9 @@ export default function PrivateLayout() {
// <Loader showLabel={true} size="size-10" /> // <Loader showLabel={true} size="size-10" />
// </div> // </div>
// ); // );
return ( return (
<SidebarProvider> <SidebarProvider open={false}>
<AppSidebar /> <AppSidebar />
<SidebarInset className="h-screen overflow-x-hidden"> <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"> <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 // 🎯 Custom Actions - Đa dạng và có điều kiện
const customActions = [ const customActions = [
{ {
key: "publist", key: "listing",
label: "Publist", label: (row: IProduct) => {
return row?.status ? "Unlist" : "Publist";
},
icon: <UploadCloud className="h-4 w-4" />, icon: <UploadCloud className="h-4 w-4" />,
action: (data: IProduct) => { action: (data: IProduct) => {
handlePublist(data); handlePublist(data);
@ -259,11 +261,15 @@ export default function List() {
enabled: !!states, enabled: !!states,
}); });
const publistMutation = useMutation({ const listingMutation = useMutation({
mutationFn: async (data: Partial<IProduct>) => { mutationFn: async (data: Partial<IProduct>) => {
await delay(300); 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) => { onSuccess: (data) => {
console.log({ data }); console.log({ data });
@ -273,7 +279,7 @@ export default function List() {
const handlePublist = async (data: Partial<IProduct>) => { const handlePublist = async (data: Partial<IProduct>) => {
toast.promise( toast.promise(
publistMutation.mutateAsync(data), // gọi function để trả về promise listingMutation.mutateAsync(data), // gọi function để trả về promise
{ {
loading: "Loading...", loading: "Loading...",
success: (result) => `${data?.title} toast has been added`, success: (result) => `${data?.title} toast has been added`,

View File

@ -30,6 +30,11 @@ export class ProductsController extends CoreController<
return this.service.publist(id); 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') @Post('publist-finish/:id')
async publistFinish( async publistFinish(
@Param('id') id: Product['id'], @Param('id') id: Product['id'],

View File

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