Zoek functie voor items toegevoegd
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import SearchButton from "@/app/search";
|
||||||
|
import { t, type Locale } from "@/lib/i18n";
|
||||||
|
import type { Item } from "@/lib/items";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function ItemsResults({
|
||||||
|
initialItems,
|
||||||
|
initialError,
|
||||||
|
locale,
|
||||||
|
}: {
|
||||||
|
initialItems: Item[];
|
||||||
|
initialError: string;
|
||||||
|
locale: Locale;
|
||||||
|
}) {
|
||||||
|
const [items, setItems] = useState(initialItems);
|
||||||
|
const [error, setError] = useState(initialError);
|
||||||
|
|
||||||
|
function handleResults(nextItems: Item[], nextError: string) {
|
||||||
|
setItems(nextItems);
|
||||||
|
setError(nextError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="inline-flex rounded-2xl bg-white p-6 shadow-md ring-1 ring-slate-200">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-slate-500">{t(locale, "articles.count")}</p>
|
||||||
|
<p className="mt-1 text-3xl font-bold text-cyan-700">{items.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-2xl bg-white shadow-lg ring-1 ring-slate-200">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-6 py-4">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800">
|
||||||
|
{t(locale, "articles.heading")}
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-input"
|
||||||
|
placeholder={t(locale, "articles.search")}
|
||||||
|
className="h-10 rounded-lg border border-slate-300 bg-white px-3 text-slate-700 outline-none focus:border-cyan-600 focus:ring-2 focus:ring-cyan-100"
|
||||||
|
/>
|
||||||
|
<SearchButton locale={locale} onResults={handleResults} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="m-6 rounded-lg border border-red-200 bg-red-50 p-4 text-red-700">
|
||||||
|
{t(locale, "articles.error")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{items.length > 0 && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-100">
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold text-slate-700">
|
||||||
|
{t(locale, "articles.code")}
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold text-slate-700">
|
||||||
|
{t(locale, "articles.descriptionColumn")}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((item) => (
|
||||||
|
<tr
|
||||||
|
key={item.ItemCode}
|
||||||
|
className="border-t border-slate-100 transition-colors hover:bg-cyan-50"
|
||||||
|
>
|
||||||
|
<td className="px-6 py-4 font-mono text-sm font-medium text-cyan-700">
|
||||||
|
{item.ItemCode}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-slate-700">{item.ItemName}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{items.length === 0 && !error && (
|
||||||
|
<div className="py-20 text-center">
|
||||||
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-slate-100">
|
||||||
|
📦
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-slate-700">
|
||||||
|
{t(locale, "articles.emptyTitle")}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-slate-500">
|
||||||
|
{t(locale, "articles.emptyDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+2
-79
@@ -1,5 +1,6 @@
|
|||||||
import LogoutButton from "@/app/logout-button";
|
import LogoutButton from "@/app/logout-button";
|
||||||
import LanguageSwitcher from "@/app/language-switcher";
|
import LanguageSwitcher from "@/app/language-switcher";
|
||||||
|
import ItemsResults from "@/app/items-results";
|
||||||
import { getItems } from "@/lib/items";
|
import { getItems } from "@/lib/items";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { getLocale, LOCALE_COOKIE, t } from "@/lib/i18n";
|
import { getLocale, LOCALE_COOKIE, t } from "@/lib/i18n";
|
||||||
@@ -31,85 +32,7 @@ export default async function ItemsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto max-w-7xl px-6 py-10">
|
<div className="mx-auto max-w-7xl px-6 py-10">
|
||||||
{/* KPI Card */}
|
<ItemsResults initialItems={items} initialError={error} locale={locale} />
|
||||||
<div className="mb-6">
|
|
||||||
<div className="inline-flex rounded-2xl bg-white p-6 shadow-md ring-1 ring-slate-200">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-slate-500">{t(locale, "articles.count")}</p>
|
|
||||||
<p className="mt-1 text-3xl font-bold text-cyan-700">
|
|
||||||
{items.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Card */}
|
|
||||||
<div className="overflow-hidden rounded-2xl bg-white shadow-lg ring-1 ring-slate-200">
|
|
||||||
<div className="border-b border-slate-200 px-6 py-4">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-800">
|
|
||||||
{t(locale, "articles.heading")}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error */}
|
|
||||||
{error && (
|
|
||||||
<div className="m-6 rounded-lg border border-red-200 bg-red-50 p-4 text-red-700">
|
|
||||||
{t(locale, "articles.error")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
{items.length > 0 && (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="bg-slate-100">
|
|
||||||
<th className="px-6 py-4 text-left text-sm font-semibold text-slate-700">
|
|
||||||
{t(locale, "articles.code")}
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-4 text-left text-sm font-semibold text-slate-700">
|
|
||||||
{t(locale, "articles.descriptionColumn")}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{items.map((item) => (
|
|
||||||
<tr
|
|
||||||
key={item.ItemCode}
|
|
||||||
className="border-t border-slate-100 transition-colors hover:bg-cyan-50"
|
|
||||||
>
|
|
||||||
<td className="px-6 py-4 font-mono text-sm font-medium text-cyan-700">
|
|
||||||
{item.ItemCode}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-6 py-4 text-slate-700">
|
|
||||||
{item.ItemName}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Empty State */}
|
|
||||||
{items.length === 0 && !error && (
|
|
||||||
<div className="py-20 text-center">
|
|
||||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-slate-100">
|
|
||||||
📦
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold text-slate-700">
|
|
||||||
{t(locale, "articles.emptyTitle")}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p className="mt-2 text-slate-500">
|
|
||||||
{t(locale, "articles.emptyDescription")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { t, type Locale } from "@/lib/i18n";
|
||||||
|
import { searchItems, type Item } from "@/lib/items";
|
||||||
|
|
||||||
|
export default function SearchButton({
|
||||||
|
locale,
|
||||||
|
onResults,
|
||||||
|
}: {
|
||||||
|
locale: Locale;
|
||||||
|
onResults: (items: Item[], error: string) => void;
|
||||||
|
}) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
async function handleSearch() {
|
||||||
|
const query = (document.getElementById("search-input") as HTMLInputElement)?.value.trim();
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await searchItems(query);
|
||||||
|
onResults(result.items, result.error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input type="text" id="search-input" placeholder={t(locale, "articles.search")} className="ml-2 rounded-lg border border-slate-300 bg-white px-3 text-slate-700 outline-none focus:border-cyan-600 focus:ring-2 focus:ring-cyan-100" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSearch}
|
||||||
|
className="inline-flex h-10 items-center rounded-lg bg-white/10 px-4 text-sm font-semibold text-white ring-1 ring-white/30 transition hover:bg-white/20"
|
||||||
|
>
|
||||||
|
{loading ? t(locale, "articles.loading") : t(locale, "articles.search")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ type TranslationKey =
|
|||||||
| "articles.emptyTitle"
|
| "articles.emptyTitle"
|
||||||
| "articles.emptyDescription"
|
| "articles.emptyDescription"
|
||||||
| "articles.error"
|
| "articles.error"
|
||||||
|
| "articles.search"
|
||||||
| "auth.logout"
|
| "auth.logout"
|
||||||
| "login.title"
|
| "login.title"
|
||||||
| "login.description"
|
| "login.description"
|
||||||
@@ -40,6 +41,7 @@ const translations: Record<Locale, Record<TranslationKey, string>> = {
|
|||||||
"articles.emptyTitle": "Geen artikelen gevonden",
|
"articles.emptyTitle": "Geen artikelen gevonden",
|
||||||
"articles.emptyDescription": "De Service Layer heeft geen artikelen teruggegeven.",
|
"articles.emptyDescription": "De Service Layer heeft geen artikelen teruggegeven.",
|
||||||
"articles.error": "Er is een fout opgetreden bij het ophalen van de artikelen.",
|
"articles.error": "Er is een fout opgetreden bij het ophalen van de artikelen.",
|
||||||
|
"articles.search": "Zoeken",
|
||||||
"auth.logout": "Uitloggen",
|
"auth.logout": "Uitloggen",
|
||||||
"login.title": "Inloggen",
|
"login.title": "Inloggen",
|
||||||
"login.description": "Log in om de artikelen te bekijken.",
|
"login.description": "Log in om de artikelen te bekijken.",
|
||||||
@@ -64,6 +66,7 @@ const translations: Record<Locale, Record<TranslationKey, string>> = {
|
|||||||
"articles.emptyTitle": "No items found",
|
"articles.emptyTitle": "No items found",
|
||||||
"articles.emptyDescription": "The Service Layer returned no items.",
|
"articles.emptyDescription": "The Service Layer returned no items.",
|
||||||
"articles.error": "An error occurred while retrieving the items.",
|
"articles.error": "An error occurred while retrieving the items.",
|
||||||
|
"articles.search": "Search",
|
||||||
"auth.logout": "Log out",
|
"auth.logout": "Log out",
|
||||||
"login.title": "Sign in",
|
"login.title": "Sign in",
|
||||||
"login.description": "Sign in to view the items.",
|
"login.description": "Sign in to view the items.",
|
||||||
@@ -88,6 +91,7 @@ const translations: Record<Locale, Record<TranslationKey, string>> = {
|
|||||||
"articles.emptyTitle": "搵唔到貨品",
|
"articles.emptyTitle": "搵唔到貨品",
|
||||||
"articles.emptyDescription": "Service Layer 冇返回任何貨品。",
|
"articles.emptyDescription": "Service Layer 冇返回任何貨品。",
|
||||||
"articles.error": "取得貨品時發生錯誤。",
|
"articles.error": "取得貨品時發生錯誤。",
|
||||||
|
"articles.search": "搜尋",
|
||||||
"auth.logout": "登出",
|
"auth.logout": "登出",
|
||||||
"login.title": "登入",
|
"login.title": "登入",
|
||||||
"login.description": "登入後查看貨品。",
|
"login.description": "登入後查看貨品。",
|
||||||
|
|||||||
+22
-1
@@ -1,7 +1,9 @@
|
|||||||
|
|
||||||
|
"use server";
|
||||||
|
|
||||||
import { executeHttpRequest } from "@sap-cloud-sdk/http-client";
|
import { executeHttpRequest } from "@sap-cloud-sdk/http-client";
|
||||||
|
|
||||||
type Item = {
|
export type Item = {
|
||||||
ItemCode: string;
|
ItemCode: string;
|
||||||
ItemName: string;
|
ItemName: string;
|
||||||
};
|
};
|
||||||
@@ -22,3 +24,22 @@ export async function getItems(): Promise<{ items: Item[]; error: string }> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function searchItems(query: string): Promise<{ items: Item[]; error: string }> {
|
||||||
|
const escapedQuery = query.replaceAll("'", "''");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await executeHttpRequest(
|
||||||
|
{ destinationName: "SAP4SL" },
|
||||||
|
{ method: "GET", url: `/b1s/v2/Items?$filter=contains(ItemName,'${escapedQuery}')` },
|
||||||
|
);
|
||||||
|
|
||||||
|
return { items: response.data.value || [], error: "" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
error: "items_request_failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user