Base para el flujo de ramas por cliente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
219 lines
7.5 KiB
TypeScript
219 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|