Files
mini-erp/mini-frontend/src/components/ui/CopyFromModal.tsx
T

218 lines
7.5 KiB
TypeScript
Raw Normal View History

import { useState, useEffect } from "react";
import { API_URL, apiFetch } from "../../utils/api";
import { PAGE_SIZE } from "../../utils/constants";
import SearchInput from "./SearchInput";
import PrimaryButton from "./PrimaryButton";
import DenseTable from "./DenseTable";
import PaginationBar from "./PaginationBar";
import type { CopyFromSource, DocumentLine } from "../../types/sapMarketing";
interface CopyFromModalProps {
isOpen: boolean;
onClose: () => void;
source: CopyFromSource;
cardCode: string;
docType: "dDocument_Items" | "dDocument_Service";
copiedDocEntries: number[];
onConfirm: (lines: DocumentLine[], sourceLabel: string) => void;
}
export default function CopyFromModal({
isOpen,
onClose,
source,
cardCode,
docType,
copiedDocEntries,
onConfirm,
}: CopyFromModalProps) {
const [data, setData] = useState<any[]>([]);
const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(false);
const [accepting, setAccepting] = useState(false);
const [error, setError] = useState("");
const [filterValue, setFilterValue] = useState("");
const [activeFilterValue, setActiveFilterValue] = useState("");
const [selectedColumn, setSelectedColumn] = useState(source.columns[0]?.key || "");
const [skip, setSkip] = useState(0);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const top = PAGE_SIZE;
useEffect(() => {
if (isOpen) {
setSelectedKeys([]);
setActiveFilterValue("");
setFilterValue("");
setSkip(0);
setError("");
setAccepting(false);
setSelectedColumn(source.columns[0]?.key || "");
}
}, [isOpen, source]);
const buildFilter = () => {
let filter = `CardCode eq '${cardCode}' and DocumentStatus eq 'bost_Open' and DocType eq '${docType}'`;
for (const entry of copiedDocEntries) {
filter += ` and DocEntry ne ${entry}`;
}
if (activeFilterValue.trim()) {
const clause = `contains(${selectedColumn}, '${activeFilterValue.trim().toUpperCase()}')`;
filter += ` and ${clause}`;
}
return filter;
};
const fetchData = async () => {
if (!isOpen) return;
setLoading(true);
setError("");
try {
const fieldsQuery = source.fields.join(",");
let url = `${API_URL}/api/${source.endpoint}?skip=${skip}&top=${top}&fields=${fieldsQuery}&filter=${encodeURIComponent(buildFilter())}`;
const response = await apiFetch(url);
if (!response.ok) throw new Error("Error al obtener el listado");
const json = await response.json();
setData(json.value || []);
setTotalCount(json["odata.count"] ?? json["@odata.count"] ?? 0);
} catch (err: any) {
setError(err.message || "Error de conexión");
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isOpen) {
fetchData();
}
}, [isOpen, skip, activeFilterValue, source.endpoint]);
const handleBuscar = () => {
setActiveFilterValue(filterValue);
setSkip(0);
};
const handleAnterior = () => setSkip(Math.max(0, skip - top));
const handleSiguiente = () => setSkip(skip + top);
const handleSelectionChange = (keys: string[]) => {
setSelectedKeys(keys);
};
const handleConfirmar = async () => {
if (selectedKeys.length === 0) return;
setAccepting(true);
setError("");
try {
const allLines: DocumentLine[] = [];
for (const docEntry of selectedKeys) {
const response = await apiFetch(
`${API_URL}/api/${source.endpoint}/${docEntry}`,
);
if (!response.ok)
throw new Error(`Error al obtener el documento #${docEntry}`);
const doc = await response.json();
const lines = (doc.DocumentLines || [])
.filter((l: any) => l.LineStatus === "bost_Open")
.map((l: any) => ({
...l,
BaseType: source.baseType,
BaseEntry: Number(docEntry),
BaseLine: l.LineNum,
BaseRef: doc.DocNum,
LineStatus: "bost_Open" as const,
LineNum: undefined,
}));
allLines.push(...lines);
}
onConfirm(allLines, source.label);
onClose();
} catch (err: any) {
setError(err.message || "Error al copiar las líneas");
} finally {
setAccepting(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 antialiased font-sans text-xs text-[var(--color-text)]">
<div className="bg-[var(--color-surface)] w-full max-w-3xl border border-[var(--color-border)] flex flex-col max-h-[80vh] min-h-[400px]">
<div className="h-10 bg-[var(--color-header-bg)] text-white flex items-center justify-between px-3 select-none shrink-0">
<span className="font-semibold">
Copiar de — {source.label}
</span>
<span className="text-[11px] text-[var(--color-header-muted)]">
Socio: {cardCode}
</span>
</div>
<div className="p-2 border-b border-[var(--color-sidebar-submenu-border)] bg-[var(--color-bg)] flex items-center gap-2 shrink-0">
<SearchInput
value={filterValue}
placeholder={`Buscar en ${source.columns.find((c) => c.key === selectedColumn)?.title || selectedColumn}`}
onChange={setFilterValue}
onKeyDown={(e) => { if (e.key === "Enter") handleBuscar(); }}
/>
<PrimaryButton onClick={handleBuscar}>Buscar</PrimaryButton>
</div>
{error && (
<div className="text-xs text-[var(--color-error)] bg-[var(--color-error-bg)] border border-[var(--color-error-border)] p-2 mx-2 mt-2 shrink-0">
{error}
</div>
)}
<div className="flex-1 overflow-auto p-2 min-h-0">
{loading ? (
<div className="text-[var(--color-primary)] p-2">
Cargando registros desde SAP Service Layer...
</div>
) : (
<DenseTable
columns={source.columns}
data={data}
selectedColumn={selectedColumn}
onColumnSelect={setSelectedColumn}
rowKey="DocEntry"
selectionMode="multiple"
selectedKeys={selectedKeys}
onSelectionChange={handleSelectionChange}
/>
)}
</div>
<div className="px-2 pt-1 shrink-0">
<PaginationBar
skip={skip}
top={top}
currentCount={data.length}
totalCount={totalCount}
loading={loading}
onPrevious={handleAnterior}
onNext={handleSiguiente}
/>
</div>
<div className="p-2 border-t border-[var(--color-border-light)] bg-[var(--color-bg)] flex justify-end gap-2 shrink-0">
<button
onClick={onClose}
className="px-4 py-1.5 bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] rounded-none cursor-pointer text-xs"
>
Cancelar
</button>
<button
onClick={handleConfirmar}
disabled={selectedKeys.length === 0 || accepting}
className="px-5 py-1.5 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white font-medium rounded-none border-none cursor-pointer text-xs disabled:opacity-50 disabled:cursor-not-allowed"
>
{accepting
? "Copiando líneas..."
: `Aceptar (${selectedKeys.length})`}
</button>
</div>
</div>
</div>
);
}