update
This commit is contained in:
parent
d12c6fbbfc
commit
3a9a4d092d
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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 và bắt lỗi nếu quá thời gian hoặc lỗi bất ngờ
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue