Compare commits
2 Commits
1b4e496860
...
53ea788142
| Author | SHA1 | Date | |
|---|---|---|---|
| 53ea788142 | |||
| 560fe06a08 |
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@uecko-presupuestador/client",
|
"name": "@uecko-presupuestador/client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"author": "Rodax Software <dev@rodax-software.com>",
|
"author": "Rodax Software <dev@rodax-software.com>",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Container, FormTextField } from "@/components";
|
import { Container, FormTextField } from "@/components";
|
||||||
import { FormSubmitButton } from "@/components/Forms/FormSubmitButton";
|
import { FormSubmitButton } from "@/components/Forms/FormSubmitButton";
|
||||||
import { UeckoLogo } from "@/components/UeckoLogo/UeckoLogo";
|
import { UeckoLogo } from "@/components/UeckoLogo/UeckoLogo";
|
||||||
import { useLogin } from "@/lib/hooks";
|
import { useIsLoggedIn, useLogin } from "@/lib/hooks";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertDescription,
|
AlertDescription,
|
||||||
@ -42,6 +42,9 @@ const languages = [
|
|||||||
|
|
||||||
export const LoginPageWithLanguageSelector = () => {
|
export const LoginPageWithLanguageSelector = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const { isSuccess: isLoggedInSuccess, data: { authenticated = false } = {} } = useIsLoggedIn();
|
||||||
|
|
||||||
const [language, setLanguage] = useState(i18n.language);
|
const [language, setLanguage] = useState(i18n.language);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -54,6 +57,12 @@ export const LoginPageWithLanguageSelector = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedInSuccess && authenticated) {
|
||||||
|
navigate("/quotes", { replace: true });
|
||||||
|
}
|
||||||
|
}, [isLoggedInSuccess, authenticated]);
|
||||||
|
|
||||||
const changeLanguage = (lng: string) => {
|
const changeLanguage = (lng: string) => {
|
||||||
i18n.changeLanguage(lng);
|
i18n.changeLanguage(lng);
|
||||||
setLanguage(lng);
|
setLanguage(lng);
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { Card, CardContent } from "@/ui";
|
|||||||
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
|
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
|
||||||
|
|
||||||
import { DataTable } from "@/components";
|
import { DataTable } from "@/components";
|
||||||
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
|
|
||||||
import { useDataTable, useDataTableContext } from "@/lib/hooks";
|
import { useDataTable, useDataTableContext } from "@/lib/hooks";
|
||||||
import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts";
|
import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts";
|
||||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||||
@ -11,6 +10,7 @@ import { t } from "i18next";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useCatalogList } from "../hooks";
|
import { useCatalogList } from "../hooks";
|
||||||
|
import { CatalogDataTableFilter } from "./CatalogDataTableFilter";
|
||||||
|
|
||||||
export const CatalogDataTable = () => {
|
export const CatalogDataTable = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -109,7 +109,7 @@ export const CatalogDataTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable table={table} paginationOptions={{ visible: true }}>
|
<DataTable table={table} paginationOptions={{ visible: true }}>
|
||||||
<DataTableToolbar table={table} />
|
<CatalogDataTableFilter table={table} />
|
||||||
</DataTable>
|
</DataTable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
135
client/src/app/catalog/components/CatalogDataTableFilter.tsx
Normal file
135
client/src/app/catalog/components/CatalogDataTableFilter.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { Table } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import { ButtonGroup } from "@/components";
|
||||||
|
import { DataTableFilterField, useDataTableContext } from "@/lib/hooks";
|
||||||
|
import { Badge, Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/ui";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { PlusIcon, SearchIcon, XIcon } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface CatalogDataTableFilterProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
table: Table<TData>;
|
||||||
|
filterFields?: DataTableFilterField<TData>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CatalogDataTableFilter<TData>({
|
||||||
|
table,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CatalogDataTableFilterProps<TData>) {
|
||||||
|
const { globalFilter, isFiltered, setGlobalFilter, resetGlobalFilter } = useDataTableContext();
|
||||||
|
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const [inputValue, setInputValue] = React.useState("");
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter" && inputValue.trim()) {
|
||||||
|
e.preventDefault();
|
||||||
|
setGlobalFilter((prev) => [...prev, inputValue.trim()]);
|
||||||
|
setInputValue("");
|
||||||
|
}
|
||||||
|
if (e.key === "Backspace" && !inputValue && globalFilter.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
setGlobalFilter((prev) => prev.slice(0, -1));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[globalFilter, inputValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeFilterTerm = React.useCallback((filterTerm: string) => {
|
||||||
|
setGlobalFilter((prev) => prev.filter((f) => f !== filterTerm));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addFilterTerm = React.useCallback(() => {
|
||||||
|
if (inputValue.trim()) {
|
||||||
|
setGlobalFilter((prev) => [...prev, inputValue.trim()]);
|
||||||
|
setInputValue("");
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [inputValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className='w-full space-y-2'>
|
||||||
|
<div className='relative flex items-center flex-1 p-2 space-x-2 border rounded-md border-input'>
|
||||||
|
<SearchIcon className='w-4 h-4 text-gray-500' />
|
||||||
|
<div className='flex flex-wrap items-center flex-1 gap-2'>
|
||||||
|
{globalFilter &&
|
||||||
|
globalFilter.map((filterTerm) => (
|
||||||
|
<Badge
|
||||||
|
key={filterTerm}
|
||||||
|
variant='default'
|
||||||
|
className='px-1 text-base font-normal rounded-sm'
|
||||||
|
>
|
||||||
|
{filterTerm}
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
onClick={() => removeFilterTerm(filterTerm)}
|
||||||
|
className='h-auto p-0 px-1 ml-1 hover:bg-transparent'
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<XIcon className='w-4 h-4' />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("catalog.filter.badget_remove_tooltip")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<span className='sr-only'>{t("catalog.filter.badget_remove_tooltip")}</span>
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
<div className='flex-1 flex items-center min-w-[300px]'>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={t("common.filter.input_placeholder")}
|
||||||
|
className='flex-1 w-full h-8 bg-transparent outline-none placeholder:text-muted-foreground'
|
||||||
|
/>
|
||||||
|
<ButtonGroup>
|
||||||
|
{isFiltered && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => resetGlobalFilter()}
|
||||||
|
className='h-8 px-2 transition-all lg:px-3'
|
||||||
|
>
|
||||||
|
<XIcon className='w-4 h-4 mr-2' />
|
||||||
|
{t("common.filter.reset_filter")}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("common.filter.reset_filter")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
onClick={addFilterTerm}
|
||||||
|
className='w-8 h-8 p-0 hover:bg-muted'
|
||||||
|
>
|
||||||
|
<PlusIcon className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>{t("common.filter.button_add_term")}</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("common.filter.button_add_term_tooltip")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='text-sm text-muted-foreground'>{t("common.filter.help_text")}</p>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ export type UseCatalogListParams = {
|
|||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
};
|
};
|
||||||
searchTerm?: string;
|
searchTerm?: string[];
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
queryOptions?: Record<string, unknown>;
|
queryOptions?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
@ -22,7 +22,7 @@ export const useCatalogList = (params: UseCatalogListParams): UseCatalogListResp
|
|||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const keys = useQueryKey();
|
const keys = useQueryKey();
|
||||||
|
|
||||||
const { pagination, searchTerm = undefined, enabled = true, queryOptions } = params;
|
const { pagination, searchTerm = [], enabled = true, queryOptions } = params;
|
||||||
|
|
||||||
return useList({
|
return useList({
|
||||||
queryKey: keys().data().resource("catalog").action("list").params(params).get(),
|
queryKey: keys().data().resource("catalog").action("list").params(params).get(),
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
|
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
|
||||||
|
|
||||||
|
import { CatalogDataTableFilter } from "@/app/catalog/components/CatalogDataTableFilter";
|
||||||
import { useCatalogList } from "@/app/catalog/hooks";
|
import { useCatalogList } from "@/app/catalog/hooks";
|
||||||
import { DataTable } from "@/components";
|
import { DataTable } from "@/components";
|
||||||
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
|
|
||||||
import { useDataTable, useDataTableContext } from "@/lib/hooks";
|
import { useDataTable, useDataTableContext } from "@/lib/hooks";
|
||||||
import { Button } from "@/ui";
|
import { Button } from "@/ui";
|
||||||
import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts";
|
import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts";
|
||||||
@ -141,7 +141,7 @@ export const CatalogPickerDataTable = ({
|
|||||||
paginationOptions={{ visible: true, enablePageSizeSelector: false }}
|
paginationOptions={{ visible: true, enablePageSizeSelector: false }}
|
||||||
footerClassName='px-10 pt-2 border-t'
|
footerClassName='px-10 pt-2 border-t'
|
||||||
>
|
>
|
||||||
<DataTableToolbar fullWidthFilter={true} table={table} />
|
<CatalogDataTableFilter table={table} />
|
||||||
</DataTable>
|
</DataTable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -215,7 +215,6 @@ export const QuoteDetailsCardEditor = ({
|
|||||||
|
|
||||||
const handleAppendCatalogArticle = useCallback(
|
const handleAppendCatalogArticle = useCallback(
|
||||||
(article: any, quantity: number = 1) => {
|
(article: any, quantity: number = 1) => {
|
||||||
console.log(article);
|
|
||||||
fieldActions.append({
|
fieldActions.append({
|
||||||
...article,
|
...article,
|
||||||
quantity: {
|
quantity: {
|
||||||
|
|||||||
@ -86,7 +86,6 @@ export default function SupportModal() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
console.log("handleClose", incidenceValue.trim());
|
|
||||||
if (incidenceValue.trim()) {
|
if (incidenceValue.trim()) {
|
||||||
setShowConfirmDialog(true);
|
setShowConfirmDialog(true);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -6,23 +6,31 @@ import { cn } from "@/lib/utils";
|
|||||||
import { Button, Input } from "@/ui";
|
import { Button, Input } from "@/ui";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { SearchIcon, XIcon } from "lucide-react";
|
import { SearchIcon, XIcon } from "lucide-react";
|
||||||
|
import { ChangeEvent } from "react";
|
||||||
import { DataTableColumnOptions } from "./DataTableColumnOptions";
|
import { DataTableColumnOptions } from "./DataTableColumnOptions";
|
||||||
|
|
||||||
interface DataTableToolbarProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
|
interface DataTableToolbarProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
filterFields?: DataTableFilterField<TData>[];
|
filterFields?: DataTableFilterField<TData>[];
|
||||||
fullWidthFilter?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableToolbar<TData>({
|
export function DataTableToolbar<TData>({
|
||||||
table,
|
table,
|
||||||
fullWidthFilter,
|
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: DataTableToolbarProps<TData>) {
|
}: DataTableToolbarProps<TData>) {
|
||||||
const { globalFilter, isFiltered, setGlobalFilter, resetGlobalFilter } = useDataTableContext();
|
const { globalFilter, isFiltered, setGlobalFilter, resetGlobalFilter } = useDataTableContext();
|
||||||
|
|
||||||
|
const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = String(event.target.value);
|
||||||
|
if (value === "") {
|
||||||
|
resetGlobalFilter();
|
||||||
|
} else {
|
||||||
|
setGlobalFilter([value]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -35,9 +43,9 @@ export function DataTableToolbar<TData>({
|
|||||||
<SearchIcon className='w-4 h-4 text-gray-500' />
|
<SearchIcon className='w-4 h-4 text-gray-500' />
|
||||||
<Input
|
<Input
|
||||||
key='global-filter'
|
key='global-filter'
|
||||||
placeholder={t("common.filter_placeholder")}
|
placeholder={t("common.filter.input_placeholder")}
|
||||||
value={globalFilter}
|
value={globalFilter}
|
||||||
onChange={(event) => setGlobalFilter(String(event.target.value))}
|
onChange={handleOnChange}
|
||||||
className={cn("h-8 w-full transition-all")}
|
className={cn("h-8 w-full transition-all")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -48,7 +56,7 @@ export function DataTableToolbar<TData>({
|
|||||||
className='h-8 px-2 transition-all lg:px-3'
|
className='h-8 px-2 transition-all lg:px-3'
|
||||||
>
|
>
|
||||||
<XIcon className='w-4 h-4 mr-2' />
|
<XIcon className='w-4 h-4 mr-2' />
|
||||||
{t("common.reset_filter")}
|
{t("common.filter.reset_filter")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -206,12 +206,6 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
|||||||
updateItem: (rowIndex: number, rowData: any, fieldName: string, value: unknown) => {
|
updateItem: (rowIndex: number, rowData: any, fieldName: string, value: unknown) => {
|
||||||
// Skip page index reset until after next rerender
|
// Skip page index reset until after next rerender
|
||||||
// skipAutoResetPageIndex();
|
// skipAutoResetPageIndex();
|
||||||
console.log({
|
|
||||||
rowIndex,
|
|
||||||
rowData,
|
|
||||||
fieldName,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
actions.update(rowIndex, { ...rowData, [`${fieldName}`]: value });
|
actions.update(rowIndex, { ...rowData, [`${fieldName}`]: value });
|
||||||
},
|
},
|
||||||
|
|||||||
@ -28,27 +28,27 @@ export const createAxiosDataProvider = (
|
|||||||
getApiAuthorization: getApiAuthLib,
|
getApiAuthorization: getApiAuthLib,
|
||||||
|
|
||||||
getList: async <R>(params: IGetListDataProviderParams): Promise<IListResponse_DTO<R>> => {
|
getList: async <R>(params: IGetListDataProviderParams): Promise<IListResponse_DTO<R>> => {
|
||||||
const { resource, quickSearchTerm, pagination, filters, sort } = params;
|
const { resource, quickSearchTerm, pagination, filters = [], sort = [] } = params;
|
||||||
|
|
||||||
const url = `${apiUrl}/${resource}`;
|
const url = `${apiUrl}/${resource}`;
|
||||||
const urlParams = new URLSearchParams();
|
const urlParams = new URLSearchParams();
|
||||||
|
|
||||||
const queryPagination = extractPaginationParams(pagination);
|
const { page, limit } = extractPaginationParams(pagination);
|
||||||
urlParams.append("page", String(queryPagination.page));
|
urlParams.append("page", String(page));
|
||||||
urlParams.append("limit", String(queryPagination.limit));
|
urlParams.append("limit", String(limit));
|
||||||
|
|
||||||
const generatedSort = extractSortParams(sort);
|
const generatedSort = extractSortParams(sort);
|
||||||
if (generatedSort && generatedSort.length > 0) {
|
if (generatedSort.length) {
|
||||||
urlParams.append("$sort_by", generatedSort.join(","));
|
urlParams.append("$sort_by", generatedSort.join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryQuickSearch = quickSearchTerm || generateQuickSearch(filters);
|
const queryQuickSearch = generateQuickSearch(quickSearchTerm, filters);
|
||||||
if (queryQuickSearch) {
|
if (queryQuickSearch.length) {
|
||||||
urlParams.append("q", queryQuickSearch);
|
urlParams.append("q", queryQuickSearch.join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryFilters = extractFilterParams(filters);
|
const queryFilters = extractFilterParams(filters);
|
||||||
if (queryFilters && queryFilters.length > 0) {
|
if (queryFilters.length) {
|
||||||
urlParams.append("$filters", queryFilters.join(","));
|
urlParams.append("$filters", queryFilters.join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ export const createAxiosDataProvider = (
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append(key || "file", file);
|
formData.append(key || "file", file);
|
||||||
|
|
||||||
console.log(file);
|
//console.log(file);
|
||||||
|
|
||||||
const response = await httpClient.post<R>(url, formData, {
|
const response = await httpClient.post<R>(url, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
@ -181,7 +181,7 @@ export const createAxiosDataProvider = (
|
|||||||
throw new Error('"url" or "path" param is missing');
|
throw new Error('"url" or "path" param is missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(apiUrl, path, url, requestUrl.toString());
|
//console.log(apiUrl, path, url, requestUrl.toString());
|
||||||
|
|
||||||
// Preparar la respuesta personalizada
|
// Preparar la respuesta personalizada
|
||||||
let customResponse;
|
let customResponse;
|
||||||
@ -389,28 +389,20 @@ export const createAxiosDataProvider = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const extractSortParams = (sort: ISortItemDataProviderParam[] = []) =>
|
const extractSortParams = (sort: ISortItemDataProviderParam[] = []) =>
|
||||||
sort.map((item) => `${item.order === "DESC" ? "-" : "+"}${item.field}`);
|
sort.map(({ field, order }) => `${order === "DESC" ? "-" : "+"}${field}`);
|
||||||
|
|
||||||
const extractFilterParams = (filters?: IFilterItemDataProviderParam[]): string[] => {
|
const extractFilterParams = (filters: IFilterItemDataProviderParam[] = []) =>
|
||||||
let queryFilters: string[] = [];
|
filters
|
||||||
if (filters) {
|
.filter(({ field }) => field !== "q")
|
||||||
queryFilters = filters
|
.map(({ field, operator, value }) => `${field}[${operator}]${value}`);
|
||||||
.filter((item) => item.field !== "q")
|
|
||||||
.map(({ field, operator, value }) => `${field}[${operator}]${value}`);
|
|
||||||
}
|
|
||||||
return queryFilters;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateQuickSearch = (filters?: IFilterItemDataProviderParam[]): string | undefined => {
|
const generateQuickSearch = (
|
||||||
let quickSearch: string | undefined = undefined;
|
quickSearchTerm: string[] = [],
|
||||||
if (filters) {
|
filters: IFilterItemDataProviderParam[] = []
|
||||||
const qsArray = filters.filter((item) => item.field === "q");
|
) =>
|
||||||
if (qsArray.length > 0) {
|
filters.find(({ field }) => field === "q")?.value
|
||||||
quickSearch = qsArray[0].value;
|
? [filters.find(({ field }) => field === "q")!.value]
|
||||||
}
|
: quickSearchTerm;
|
||||||
}
|
|
||||||
return quickSearch;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractPaginationParams = (pagination?: IPaginationDataProviderParam) => {
|
const extractPaginationParams = (pagination?: IPaginationDataProviderParam) => {
|
||||||
const { pageIndex = INITIAL_PAGE_INDEX, pageSize = INITIAL_PAGE_SIZE } = pagination || {};
|
const { pageIndex = INITIAL_PAGE_INDEX, pageSize = INITIAL_PAGE_SIZE } = pagination || {};
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export interface IFilterItemDataProviderParam {
|
|||||||
|
|
||||||
export interface IGetListDataProviderParams {
|
export interface IGetListDataProviderParams {
|
||||||
resource: string;
|
resource: string;
|
||||||
quickSearchTerm?: string;
|
quickSearchTerm?: string[];
|
||||||
pagination?: IPaginationDataProviderParam;
|
pagination?: IPaginationDataProviderParam;
|
||||||
sort?: ISortItemDataProviderParam[];
|
sort?: ISortItemDataProviderParam[];
|
||||||
filters?: IFilterItemDataProviderParam[];
|
filters?: IFilterItemDataProviderParam[];
|
||||||
|
|||||||
@ -16,8 +16,8 @@ export interface IDataTableContextState {
|
|||||||
setPagination: (newPagination: PaginationState) => void;
|
setPagination: (newPagination: PaginationState) => void;
|
||||||
sorting: SortingState;
|
sorting: SortingState;
|
||||||
setSorting: Dispatch<SetStateAction<SortingState>>;
|
setSorting: Dispatch<SetStateAction<SortingState>>;
|
||||||
globalFilter?: string;
|
globalFilter: string[];
|
||||||
setGlobalFilter: (newGlobalFilter: string) => void;
|
setGlobalFilter: Dispatch<SetStateAction<string[]>>;
|
||||||
resetGlobalFilter: () => void;
|
resetGlobalFilter: () => void;
|
||||||
isFiltered: boolean;
|
isFiltered: boolean;
|
||||||
}
|
}
|
||||||
@ -26,13 +26,13 @@ export const DataTableContext = createContext<IDataTableContextState | null>(nul
|
|||||||
|
|
||||||
export const DataTableProvider = ({
|
export const DataTableProvider = ({
|
||||||
syncWithLocation = true,
|
syncWithLocation = true,
|
||||||
initialGlobalFilter = undefined,
|
initialGlobalFilter = [],
|
||||||
initialPageIndex,
|
initialPageIndex,
|
||||||
initialPageSize,
|
initialPageSize,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<{
|
}: PropsWithChildren<{
|
||||||
syncWithLocation?: boolean;
|
syncWithLocation?: boolean;
|
||||||
initialGlobalFilter?: string;
|
initialGlobalFilter?: string[];
|
||||||
initialPageIndex?: number;
|
initialPageIndex?: number;
|
||||||
initialPageSize?: number;
|
initialPageSize?: number;
|
||||||
}>) => {
|
}>) => {
|
||||||
@ -41,11 +41,11 @@ export const DataTableProvider = ({
|
|||||||
initialPageIndex,
|
initialPageIndex,
|
||||||
initialPageSize,
|
initialPageSize,
|
||||||
});
|
});
|
||||||
const [globalFilter, setGlobalFilter] = useState<string | undefined>(initialGlobalFilter);
|
const [globalFilter, setGlobalFilter] = useState<string[]>(initialGlobalFilter || []);
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const isFiltered = useMemo(() => Boolean(globalFilter && globalFilter.length), [globalFilter]);
|
const isFiltered = useMemo(() => Boolean(globalFilter && globalFilter.length), [globalFilter]);
|
||||||
const resetGlobalFilter = useCallback(() => setGlobalFilter(""), []);
|
const resetGlobalFilter = useCallback(() => setGlobalFilter([]), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTableContext.Provider
|
<DataTableContext.Provider
|
||||||
|
|||||||
@ -153,7 +153,7 @@ export function useDataTable<TData, TValue>({
|
|||||||
const sortingUpdater: OnChangeFn<SortingState> = (updater) => {
|
const sortingUpdater: OnChangeFn<SortingState> = (updater) => {
|
||||||
if (typeof updater === "function") {
|
if (typeof updater === "function") {
|
||||||
const newSorting = updater(sorting);
|
const newSorting = updater(sorting);
|
||||||
console.log(newSorting);
|
//console.log(newSorting);
|
||||||
//setSorting(newSorting);
|
//setSorting(newSorting);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => {
|
|||||||
<CustomDialog
|
<CustomDialog
|
||||||
//type='warning'
|
//type='warning'
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
console.log("onCancel");
|
//console.log("onCancel");
|
||||||
onCancel();
|
onCancel();
|
||||||
}}
|
}}
|
||||||
onConfirm={() => onConfirm()}
|
onConfirm={() => onConfirm()}
|
||||||
|
|||||||
@ -29,8 +29,6 @@
|
|||||||
"go_to_prev_page": "Go to previous page",
|
"go_to_prev_page": "Go to previous page",
|
||||||
"go_to_next_page": "Go to next page",
|
"go_to_next_page": "Go to next page",
|
||||||
"go_to_last_page": "Go to last page",
|
"go_to_last_page": "Go to last page",
|
||||||
"filter_placeholder": "Type here to filter...",
|
|
||||||
"reset_filter": "Reset filter",
|
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"open_menu": "Open menu",
|
"open_menu": "Open menu",
|
||||||
@ -57,7 +55,16 @@
|
|||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"print": "Print",
|
"print": "Print",
|
||||||
"disable_preview": "Disable preview",
|
"disable_preview": "Disable preview",
|
||||||
"enable_preview": "Enable preview"
|
"enable_preview": "Enable preview",
|
||||||
|
"filter": {
|
||||||
|
"reset_filter": "Reset filter",
|
||||||
|
"badget_remove_tooltip": "Remove this filter term",
|
||||||
|
"input_placeholder": "Type here to filter...",
|
||||||
|
"button_add_term": "Add filter term",
|
||||||
|
"button_add_term_tooltip": "Add filter term (or press Enter)",
|
||||||
|
"button_remove_all": "Remove all terms from filter",
|
||||||
|
"help_text": "Press Enter or click the + button to add a term to the filter. Use multiple terms for a broader search."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"loading_indicator": {
|
"loading_indicator": {
|
||||||
|
|||||||
@ -29,8 +29,6 @@
|
|||||||
"go_to_prev_page": "Ir a la página anterior",
|
"go_to_prev_page": "Ir a la página anterior",
|
||||||
"go_to_next_page": "Ir a la página siguiente",
|
"go_to_next_page": "Ir a la página siguiente",
|
||||||
"go_to_last_page": "Ir a la última página",
|
"go_to_last_page": "Ir a la última página",
|
||||||
"filter_placeholder": "Escribe aquí para filtrar...",
|
|
||||||
"reset_filter": "Quitar el filtro",
|
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"actions": "Acciones",
|
"actions": "Acciones",
|
||||||
"open_menu": "Abrir el menú",
|
"open_menu": "Abrir el menú",
|
||||||
@ -57,7 +55,16 @@
|
|||||||
"duplicate": "Duplicar",
|
"duplicate": "Duplicar",
|
||||||
"print": "Imprimir",
|
"print": "Imprimir",
|
||||||
"disable_preview": "Ocultar vista previa",
|
"disable_preview": "Ocultar vista previa",
|
||||||
"enable_preview": "Mostrar vista previa"
|
"enable_preview": "Mostrar vista previa",
|
||||||
|
"filter": {
|
||||||
|
"reset_filter": "Quitar el filtro",
|
||||||
|
"badget_remove_tooltip": "Quitar este término del filtro",
|
||||||
|
"input_placeholder": "Escribe aquí para filtrar...",
|
||||||
|
"button_add_term": "Añadir término al filtro",
|
||||||
|
"button_add_term_tooltip": "Añadir término al filtro (o pulsa Enter)",
|
||||||
|
"button_remove_all": "Quitar todos los términos del filtro",
|
||||||
|
"help_text": "Presiona Enter o haz clic en el botón + para añadir un término al filtro. Usa múltiples términos para una búsqueda más amplia."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"LoadingIndicator": {
|
"LoadingIndicator": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uecko-presupuestador",
|
"name": "uecko-presupuestador",
|
||||||
"version": "1.0.6",
|
"version": "1.0.8",
|
||||||
"author": "Rodax Software <dev@rodax-software.com>",
|
"author": "Rodax Software <dev@rodax-software.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@uecko-presupuestador/server",
|
"name": "@uecko-presupuestador/server",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.6",
|
"version": "1.0.8",
|
||||||
"author": "Rodax Software <dev@rodax-software.com>",
|
"author": "Rodax Software <dev@rodax-software.com>",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export class ListArticlesController extends ExpressController {
|
|||||||
limit: Joi.number().optional(),
|
limit: Joi.number().optional(),
|
||||||
$sort_by: Joi.string().optional(),
|
$sort_by: Joi.string().optional(),
|
||||||
$filters: Joi.string().optional(),
|
$filters: Joi.string().optional(),
|
||||||
q: Joi.string().optional(),
|
q: Joi.string().optional().allow(null).allow(""),
|
||||||
}).optional();
|
}).optional();
|
||||||
|
|
||||||
return RuleValidator.validate(schema, query);
|
return RuleValidator.validate(schema, query);
|
||||||
@ -72,7 +72,6 @@ export class ListArticlesController extends ExpressController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
||||||
|
|
||||||
const result: ListArticlesResult = await this.useCase.execute({
|
const result: ListArticlesResult = await this.useCase.execute({
|
||||||
queryCriteria,
|
queryCriteria,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -81,17 +81,21 @@ export default (sequelize: Sequelize) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
scopes: {
|
scopes: {
|
||||||
quickSearch(value) {
|
quickSearch(values: string[]) {
|
||||||
|
/**
|
||||||
|
* {
|
||||||
|
[Op.or]: [
|
||||||
|
{ description: { [Op.like]: '%apple%' } },
|
||||||
|
{ description: { [Op.like]: '%banana%' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return {
|
return {
|
||||||
where: {
|
where: {
|
||||||
[Op.or]: {
|
[Op.and]: values
|
||||||
reference: {
|
.map((value) => [{ description: { [Op.like]: `%${value}%` } }])
|
||||||
[Op.like]: `%${value}%`,
|
.flat(),
|
||||||
},
|
|
||||||
description: {
|
|
||||||
[Op.like]: `%${value}%`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
order: [["description", "ASC"]],
|
order: [["description", "ASC"]],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,13 +15,11 @@ export interface IQueryCriteriaServiceProps {
|
|||||||
sort_by: string;
|
sort_by: string;
|
||||||
fields: string;
|
fields: string;
|
||||||
filters: string;
|
filters: string;
|
||||||
q: string;
|
q: string; // <- si viene separado por comas, es un array
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryCriteriaService extends ApplicationService {
|
export class QueryCriteriaService extends ApplicationService {
|
||||||
public static parse(
|
public static parse(params: Partial<IQueryCriteriaServiceProps>): IQueryCriteria {
|
||||||
params: Partial<IQueryCriteriaServiceProps>,
|
|
||||||
): IQueryCriteria {
|
|
||||||
const {
|
const {
|
||||||
page = undefined,
|
page = undefined,
|
||||||
limit = undefined,
|
limit = undefined,
|
||||||
@ -42,8 +40,7 @@ export class QueryCriteriaService extends ApplicationService {
|
|||||||
|
|
||||||
const _order: OrderCriteria = QueryCriteriaService.parseOrder(sort_by);
|
const _order: OrderCriteria = QueryCriteriaService.parseOrder(sort_by);
|
||||||
|
|
||||||
const _quickSearch: QuickSearchCriteria =
|
const _quickSearch: QuickSearchCriteria = QueryCriteriaService.parseQuickSearch(q);
|
||||||
QueryCriteriaService.parseQuickSearch(q);
|
|
||||||
|
|
||||||
return QueryCriteria.create({
|
return QueryCriteria.create({
|
||||||
pagination: _pagination,
|
pagination: _pagination,
|
||||||
@ -89,7 +86,7 @@ export class QueryCriteriaService extends ApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected static parseQuickSearch(quickSearch?: string): QuickSearchCriteria {
|
protected static parseQuickSearch(quickSearch?: string): QuickSearchCriteria {
|
||||||
const quickSearchOrError = QuickSearchCriteria.create(quickSearch);
|
const quickSearchOrError = QuickSearchCriteria.create(quickSearch?.split(","));
|
||||||
if (quickSearchOrError.isFailure) {
|
if (quickSearchOrError.isFailure) {
|
||||||
throw quickSearchOrError.error;
|
throw quickSearchOrError.error;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,8 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
|
|||||||
...params,
|
...params,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(args.where);
|
||||||
|
|
||||||
const result = _model.findAndCountAll(args);
|
const result = _model.findAndCountAll(args);
|
||||||
|
|
||||||
console.timeEnd("_findAll");
|
console.timeEnd("_findAll");
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export class SequelizeQueryBuilder implements ISequelizeQueryBuilder {
|
|||||||
if (!quickSearchCriteria.isEmpty()) {
|
if (!quickSearchCriteria.isEmpty()) {
|
||||||
if (_model && _model.options.scopes && _model.options.scopes["quickSearch"]) {
|
if (_model && _model.options.scopes && _model.options.scopes["quickSearch"]) {
|
||||||
_model = _model.scope({
|
_model = _model.scope({
|
||||||
method: ["quickSearch", quickSearchCriteria.value],
|
method: ["quickSearch", quickSearchCriteria.searchTerms],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,62 +2,72 @@ import Joi from "joi";
|
|||||||
import { UndefinedOr } from "../../../../../../utilities";
|
import { UndefinedOr } from "../../../../../../utilities";
|
||||||
import { RuleValidator } from "../../../RuleValidator";
|
import { RuleValidator } from "../../../RuleValidator";
|
||||||
import { Result } from "../../Result";
|
import { Result } from "../../Result";
|
||||||
import { StringValueObject } from "../../StringValueObject";
|
import { ValueObject } from "../../ValueObject";
|
||||||
|
|
||||||
export interface IQuickSearchCriteria {
|
export interface IQuickSearchCriteria {
|
||||||
searchTerm: string;
|
searchTerms: string[];
|
||||||
toJSON(): string;
|
isEmpty(): boolean;
|
||||||
|
toStringArray(): string[];
|
||||||
toString(): string;
|
toString(): string;
|
||||||
|
toJSON(): string;
|
||||||
|
toPrimitive(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QuickSearchCriteria
|
export class QuickSearchCriteria
|
||||||
extends StringValueObject
|
extends ValueObject<UndefinedOr<string[]>>
|
||||||
implements IQuickSearchCriteria
|
implements IQuickSearchCriteria
|
||||||
{
|
{
|
||||||
protected static validate(value: UndefinedOr<string>) {
|
protected static validate(value: UndefinedOr<string[]>) {
|
||||||
const searchString = value;
|
const searchStringArray = value;
|
||||||
|
const schema = Joi.array().items(Joi.string().trim().allow("")).default([]);
|
||||||
|
|
||||||
if (
|
const stringArrayOrError = RuleValidator.validate<string[]>(schema, searchStringArray);
|
||||||
RuleValidator.validate(
|
|
||||||
RuleValidator.RULE_NOT_NULL_OR_UNDEFINED,
|
|
||||||
searchString,
|
|
||||||
).isSuccess
|
|
||||||
) {
|
|
||||||
const stringOrError = RuleValidator.validate<string>(
|
|
||||||
RuleValidator.RULE_IS_TYPE_STRING,
|
|
||||||
searchString,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (stringOrError.isFailure) {
|
if (stringArrayOrError.isFailure) {
|
||||||
return stringOrError;
|
return stringArrayOrError;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok(searchString);
|
return Result.ok(searchStringArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static create(value: UndefinedOr<string>) {
|
public static create(value: UndefinedOr<string[]>) {
|
||||||
const stringOrError = this.validate(value);
|
const stringArrayOrError = this.validate(value);
|
||||||
|
|
||||||
if (stringOrError.isFailure) {
|
if (stringArrayOrError.isFailure) {
|
||||||
return Result.fail(stringOrError.error);
|
return Result.fail(stringArrayOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _term = QuickSearchCriteria.sanitize(stringOrError.object);
|
const sanitizedTerms = QuickSearchCriteria.sanitize(stringArrayOrError.object);
|
||||||
|
|
||||||
return Result.ok<QuickSearchCriteria>(new QuickSearchCriteria(_term));
|
return Result.ok<QuickSearchCriteria>(new QuickSearchCriteria(sanitizedTerms));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static sanitize(searchTerm: UndefinedOr<string>): string {
|
private static sanitize(terms: string[] | undefined): string[] {
|
||||||
return String(Joi.string().default("").trim().validate(searchTerm).value);
|
return terms ? terms.map((term) => term.trim()).filter((term) => term.length > 0) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get searchTerm(): string {
|
get value(): UndefinedOr<string[]> {
|
||||||
return this.toString();
|
return !this.isEmpty() ? this.props : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get searchTerms(): string[] {
|
||||||
|
return this.toStringArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty = (): boolean => {
|
||||||
|
return this.props ? this.props.length === 0 : true;
|
||||||
|
};
|
||||||
|
|
||||||
|
public toStringArray = (): string[] => {
|
||||||
|
return this.props ? [...this.props] : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
public toString = (): string => {
|
||||||
|
return this.toStringArray().toString();
|
||||||
|
};
|
||||||
|
|
||||||
public toJSON(): string {
|
public toJSON(): string {
|
||||||
return JSON.stringify(this.toString());
|
return JSON.stringify(this.toStringArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public toPrimitive(): string {
|
public toPrimitive(): string {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@uecko-presupuestador/shared",
|
"name": "@uecko-presupuestador/shared",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.0.6",
|
"version": "1.0.8",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"author": "Rodax Software <dev@rodax-software.com>",
|
"author": "Rodax Software <dev@rodax-software.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user