Fix product actions and warehouse onboarding
This commit is contained in:
@ -137,7 +137,7 @@ export function WarehouseForm({
|
||||
postalCode: form.postalCode || null,
|
||||
latitude: form.latitude ? parseFloat(form.latitude) : null,
|
||||
longitude: form.longitude ? parseFloat(form.longitude) : null,
|
||||
warehouseType: form.warehouseType || null,
|
||||
warehouseType: "INA",
|
||||
};
|
||||
|
||||
try {
|
||||
@ -296,19 +296,6 @@ export function WarehouseForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Warehouse Type */}
|
||||
<div>
|
||||
<label className={labelCls}>Tipe Warehouse</label>
|
||||
<select
|
||||
value={form.warehouseType}
|
||||
onChange={(e) => update({ warehouseType: e.target.value })}
|
||||
className={inputCls}
|
||||
>
|
||||
<option value="INA">INA</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Lat / Lng */}
|
||||
<div>
|
||||
<label className={labelCls}>Latitude</label>
|
||||
|
||||
@ -229,6 +229,19 @@ function toStr(v: string | number | null | undefined): string {
|
||||
return String(v);
|
||||
}
|
||||
|
||||
function hasBackendError(result: unknown): result is { responseCode?: string; responseDesc?: string; error?: string } {
|
||||
if (!result || typeof result !== "object") return false;
|
||||
const responseCode = "responseCode" in result ? String(result.responseCode ?? "") : "";
|
||||
return Boolean(responseCode && responseCode !== "0000");
|
||||
}
|
||||
|
||||
function backendErrorMessage(
|
||||
result: { responseDesc?: string; error?: string } | null | undefined,
|
||||
fallback: string
|
||||
) {
|
||||
return result?.responseDesc || result?.error || fallback;
|
||||
}
|
||||
|
||||
function toProductFiles(items: ApiProduct["productFiles"]): Array<{ id: string; name: string }> {
|
||||
if (!Array.isArray(items)) return [];
|
||||
|
||||
@ -1351,9 +1364,9 @@ function EditProductPageInner() {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok) {
|
||||
if (!res.ok || hasBackendError(result)) {
|
||||
setErrorLog({ request: payload, response: result });
|
||||
throw new Error(result?.responseDesc || "Gagal menyimpan draft");
|
||||
throw new Error(backendErrorMessage(result, "Gagal menyimpan draft"));
|
||||
}
|
||||
|
||||
setSaveSuccess(true);
|
||||
@ -1382,9 +1395,9 @@ function EditProductPageInner() {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok) {
|
||||
if (!res.ok || hasBackendError(result)) {
|
||||
setErrorLog({ request: payload, response: result });
|
||||
throw new Error(result?.responseDesc || "Gagal mempublikasikan produk");
|
||||
throw new Error(backendErrorMessage(result, "Gagal mempublikasikan produk"));
|
||||
}
|
||||
|
||||
setPublishSuccess(true);
|
||||
@ -1413,9 +1426,9 @@ function EditProductPageInner() {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok) {
|
||||
if (!res.ok || hasBackendError(result)) {
|
||||
setErrorLog({ request: payload, response: result });
|
||||
throw new Error(result?.responseDesc || "Gagal menyimpan produk");
|
||||
throw new Error(backendErrorMessage(result, "Gagal menyimpan produk"));
|
||||
}
|
||||
|
||||
setSaveSuccess(true);
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { Suspense, useEffect, useState, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useLanguage } from "@/lib/i18n-context";
|
||||
import { getProductEffectivePoints } from "@/lib/product-variants";
|
||||
@ -52,6 +53,8 @@ interface ProductMeasurementRef {
|
||||
interface ProductModelRef {
|
||||
id?: string | number | null;
|
||||
productModelId?: string | number | null;
|
||||
image?: string | null;
|
||||
imageId?: string | null;
|
||||
name?: string | null;
|
||||
sku?: string | null;
|
||||
isMeasurement?: boolean | null;
|
||||
@ -61,6 +64,9 @@ interface ProductModelRef {
|
||||
}
|
||||
|
||||
interface ProductDetailRef {
|
||||
image?: string | null;
|
||||
imageId?: string | null;
|
||||
productImages?: Array<{ image?: string | null; imageId?: string | null; sequence?: number | null }> | null;
|
||||
productModels?: ProductModelRef[];
|
||||
}
|
||||
|
||||
@ -139,12 +145,33 @@ function hasMissingListPrice(product: ProductRow) {
|
||||
return (!Number.isFinite(minPrice) || !Number.isFinite(maxPrice) || (minPrice <= 0 && maxPrice <= 0));
|
||||
}
|
||||
|
||||
function hasMissingListImage(product: ProductRow) {
|
||||
return !product.image;
|
||||
}
|
||||
|
||||
function getDetailFallbackImage(detail: ProductDetailRef) {
|
||||
if (detail.image) return detail.image;
|
||||
|
||||
const galleryImage = Array.isArray(detail.productImages)
|
||||
? detail.productImages
|
||||
.slice()
|
||||
.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0))
|
||||
.find((item) => item.image)?.image
|
||||
: "";
|
||||
|
||||
if (galleryImage) return galleryImage;
|
||||
|
||||
return Array.isArray(detail.productModels)
|
||||
? detail.productModels.find((model) => model.image)?.image || ""
|
||||
: "";
|
||||
}
|
||||
|
||||
async function hydrateRowsWithEffectivePrice(
|
||||
rows: ProductRow[],
|
||||
token: string,
|
||||
query: string
|
||||
) {
|
||||
const targets = rows.filter(hasMissingListPrice);
|
||||
const targets = rows.filter((row) => hasMissingListPrice(row) || hasMissingListImage(row));
|
||||
if (targets.length === 0) return rows;
|
||||
|
||||
const details = await Promise.all(
|
||||
@ -167,25 +194,32 @@ async function hydrateRowsWithEffectivePrice(
|
||||
|
||||
return rows.map((row) => {
|
||||
const detail = detailMap.get(row.id);
|
||||
if (!detail || !Array.isArray(detail.productModels)) return row;
|
||||
if (!detail) return row;
|
||||
|
||||
const prices = getProductEffectivePoints(detail.productModels)
|
||||
const fallbackImage = row.image || getDetailFallbackImage(detail);
|
||||
const productModels = Array.isArray(detail.productModels) ? detail.productModels : row.productModels;
|
||||
|
||||
const prices = Array.isArray(productModels)
|
||||
? getProductEffectivePoints(productModels)
|
||||
.map((point) => point.price)
|
||||
.filter((value): value is number => value !== undefined && value > 0)
|
||||
.sort((a, b) => a - b);
|
||||
.sort((a, b) => a - b)
|
||||
: [];
|
||||
|
||||
if (prices.length === 0) {
|
||||
return {
|
||||
...row,
|
||||
productModels: detail.productModels,
|
||||
image: fallbackImage || row.image,
|
||||
productModels,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
image: fallbackImage || row.image,
|
||||
minPrice: prices[0],
|
||||
maxPrice: prices[prices.length - 1],
|
||||
productModels: detail.productModels,
|
||||
productModels,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -747,7 +781,11 @@ function ProductsPageInner() {
|
||||
const [stockPriceTarget, setStockPriceTarget] = useState<StockPriceTarget | null>(null);
|
||||
const [warehouseLookupMap, setWarehouseLookupMap] = useState<Record<string, WarehouseLookup>>({});
|
||||
const [openActionMenuId, setOpenActionMenuId] = useState<string | null>(null);
|
||||
const actionMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const [actionMenuPosition, setActionMenuPosition] = useState<{
|
||||
left: number;
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
} | null>(null);
|
||||
|
||||
// Reset to page 1 when tab changes
|
||||
useEffect(() => {
|
||||
@ -760,27 +798,88 @@ function ProductsPageInner() {
|
||||
|
||||
useEffect(() => {
|
||||
function handlePointerDown(event: MouseEvent) {
|
||||
if (!actionMenuRef.current) return;
|
||||
if (!actionMenuRef.current.contains(event.target as Node)) {
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (!target?.closest("[data-product-action-menu]")) {
|
||||
setOpenActionMenuId(null);
|
||||
setActionMenuPosition(null);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
setOpenActionMenuId(null);
|
||||
setActionMenuPosition(null);
|
||||
}
|
||||
}
|
||||
|
||||
function handleViewportChange() {
|
||||
setOpenActionMenuId(null);
|
||||
setActionMenuPosition(null);
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handlePointerDown);
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
window.addEventListener("resize", handleViewportChange);
|
||||
window.addEventListener("scroll", handleViewportChange, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handlePointerDown);
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
window.removeEventListener("resize", handleViewportChange);
|
||||
window.removeEventListener("scroll", handleViewportChange, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function closeActionMenu() {
|
||||
setOpenActionMenuId(null);
|
||||
setActionMenuPosition(null);
|
||||
}
|
||||
|
||||
function toggleActionMenu(productId: string, event: React.MouseEvent<HTMLButtonElement>) {
|
||||
if (openActionMenuId === productId) {
|
||||
closeActionMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const menuWidth = 192;
|
||||
const estimatedMenuHeight = 240;
|
||||
const viewportPadding = 12;
|
||||
const shouldOpenUp = rect.bottom + estimatedMenuHeight > window.innerHeight;
|
||||
|
||||
setActionMenuPosition({
|
||||
left: Math.max(
|
||||
viewportPadding,
|
||||
Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - viewportPadding)
|
||||
),
|
||||
...(shouldOpenUp
|
||||
? { bottom: window.innerHeight - rect.top + 8 }
|
||||
: { top: rect.bottom + 8 }),
|
||||
});
|
||||
setOpenActionMenuId(productId);
|
||||
}
|
||||
|
||||
function renderActionMenu(productId: string, children: ReactNode) {
|
||||
if (
|
||||
openActionMenuId !== productId ||
|
||||
!actionMenuPosition ||
|
||||
typeof document === "undefined"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
data-product-action-menu
|
||||
className="fixed z-[80] w-48 overflow-hidden rounded-2xl border border-outline-variant/15 bg-surface-container-lowest py-2 shadow-2xl"
|
||||
style={actionMenuPosition}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function loadWarehouses() {
|
||||
try {
|
||||
@ -1407,22 +1506,17 @@ function ProductsPageInner() {
|
||||
{p.deletedByAdmin}
|
||||
</span>
|
||||
) : (
|
||||
<div className="relative" ref={openActionMenuId === product.id ? actionMenuRef : null}>
|
||||
<div className="relative" data-product-action-menu>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOpenActionMenuId((current) =>
|
||||
current === product.id ? null : product.id
|
||||
)
|
||||
}
|
||||
onClick={(event) => toggleActionMenu(product.id, event)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-xl border border-outline-variant/20 bg-surface-container-low text-on-surface transition-colors hover:border-primary/30 hover:text-primary"
|
||||
aria-label={`Open actions for ${product.name}`}
|
||||
aria-expanded={openActionMenuId === product.id}
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl">more_vert</span>
|
||||
</button>
|
||||
{openActionMenuId === product.id ? (
|
||||
<div className="absolute right-0 top-11 z-20 w-48 overflow-hidden rounded-2xl border border-outline-variant/15 bg-surface-container-lowest py-2 shadow-2xl">
|
||||
{renderActionMenu(product.id, <>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRestore(product.id)}
|
||||
@ -1432,27 +1526,21 @@ function ProductsPageInner() {
|
||||
<span className="material-symbols-outlined text-lg text-tertiary">settings_backup_restore</span>
|
||||
{restoringId === product.id ? p.publishing : p.restore}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>)}
|
||||
</div>
|
||||
)
|
||||
) : productState === "UNPUBLISHED" ? (
|
||||
<div className="relative" ref={openActionMenuId === product.id ? actionMenuRef : null}>
|
||||
<div className="relative" data-product-action-menu>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOpenActionMenuId((current) =>
|
||||
current === product.id ? null : product.id
|
||||
)
|
||||
}
|
||||
onClick={(event) => toggleActionMenu(product.id, event)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-xl border border-outline-variant/20 bg-surface-container-low text-on-surface transition-colors hover:border-primary/30 hover:text-primary"
|
||||
aria-label={`Open actions for ${product.name}`}
|
||||
aria-expanded={openActionMenuId === product.id}
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl">more_vert</span>
|
||||
</button>
|
||||
{openActionMenuId === product.id ? (
|
||||
<div className="absolute right-0 top-11 z-20 w-48 overflow-hidden rounded-2xl border border-outline-variant/15 bg-surface-container-lowest py-2 shadow-2xl">
|
||||
{renderActionMenu(product.id, <>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handlePublish(product.id)}
|
||||
@ -1470,26 +1558,20 @@ function ProductsPageInner() {
|
||||
<span className="material-symbols-outlined text-lg text-secondary">price_change</span>
|
||||
{p.editStockPrice}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>)}
|
||||
</div>
|
||||
) : productState === "DELETED_BY_SELLER" ? (
|
||||
<div className="relative" ref={openActionMenuId === product.id ? actionMenuRef : null}>
|
||||
<div className="relative" data-product-action-menu>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOpenActionMenuId((current) =>
|
||||
current === product.id ? null : product.id
|
||||
)
|
||||
}
|
||||
onClick={(event) => toggleActionMenu(product.id, event)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-xl border border-outline-variant/20 bg-surface-container-low text-on-surface transition-colors hover:border-primary/30 hover:text-primary"
|
||||
aria-label={`Open actions for ${product.name}`}
|
||||
aria-expanded={openActionMenuId === product.id}
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl">more_vert</span>
|
||||
</button>
|
||||
{openActionMenuId === product.id ? (
|
||||
<div className="absolute right-0 top-11 z-20 w-48 overflow-hidden rounded-2xl border border-outline-variant/15 bg-surface-container-lowest py-2 shadow-2xl">
|
||||
{renderActionMenu(product.id, <>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRestore(product.id)}
|
||||
@ -1507,26 +1589,20 @@ function ProductsPageInner() {
|
||||
<span className="material-symbols-outlined text-lg text-secondary">price_change</span>
|
||||
{p.editStockPrice}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative" ref={openActionMenuId === product.id ? actionMenuRef : null}>
|
||||
<div className="relative" data-product-action-menu>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOpenActionMenuId((current) =>
|
||||
current === product.id ? null : product.id
|
||||
)
|
||||
}
|
||||
onClick={(event) => toggleActionMenu(product.id, event)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-xl border border-outline-variant/20 bg-surface-container-low text-on-surface transition-colors hover:border-primary/30 hover:text-primary"
|
||||
aria-label={`Open actions for ${product.name}`}
|
||||
aria-expanded={openActionMenuId === product.id}
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl">more_vert</span>
|
||||
</button>
|
||||
{openActionMenuId === product.id ? (
|
||||
<div className="absolute right-0 top-11 z-20 w-48 overflow-hidden rounded-2xl border border-outline-variant/15 bg-surface-container-lowest py-2 shadow-2xl">
|
||||
{renderActionMenu(product.id, <>
|
||||
{!isInReviewTab ? (
|
||||
!isDeletedTab ? (
|
||||
<Link
|
||||
@ -1579,8 +1655,7 @@ function ProductsPageInner() {
|
||||
<span className="material-symbols-outlined text-lg">delete</span>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { UploadField } from "@/components/upload-field";
|
||||
import { COUNTRIES } from "@/lib/countries";
|
||||
import { useLanguage } from "@/lib/i18n-context";
|
||||
|
||||
const ONBOARDING_BUSINESS_STORAGE_KEY = "onboardingBusinessDraft";
|
||||
@ -34,6 +35,14 @@ type WarehouseListRow = {
|
||||
warehouseType?: string | null;
|
||||
};
|
||||
|
||||
type Province = { id: string; name: string };
|
||||
type City = { id: string; name: string };
|
||||
|
||||
function normalizeToken(token: string) {
|
||||
if (!token) return "";
|
||||
return token.startsWith("Bearer ") ? token : `Bearer ${token}`;
|
||||
}
|
||||
|
||||
export default function StoreDetailPage() {
|
||||
const router = useRouter();
|
||||
const { t } = useLanguage();
|
||||
@ -54,11 +63,19 @@ export default function StoreDetailPage() {
|
||||
warehouseType: "INA",
|
||||
country: "Indonesia",
|
||||
province: "",
|
||||
provinceId: "",
|
||||
city: "",
|
||||
cityId: "",
|
||||
postalCode: "",
|
||||
latitude: "",
|
||||
longitude: "",
|
||||
});
|
||||
const [provinces, setProvinces] = useState<Province[]>([]);
|
||||
const [cities, setCities] = useState<City[]>([]);
|
||||
const [loadingProvinces, setLoadingProvinces] = useState(false);
|
||||
const [loadingCities, setLoadingCities] = useState(false);
|
||||
|
||||
const isIndonesia = warehouse.country === "Indonesia";
|
||||
|
||||
function getToken() {
|
||||
return (
|
||||
@ -66,6 +83,10 @@ export default function StoreDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function updateWarehouse(patch: Partial<typeof warehouse>) {
|
||||
setWarehouse((prev) => ({ ...prev, ...patch }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const token = getToken();
|
||||
const businessDraftRaw = sessionStorage.getItem(ONBOARDING_BUSINESS_STORAGE_KEY);
|
||||
@ -124,6 +145,63 @@ export default function StoreDetailPage() {
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isIndonesia) {
|
||||
setProvinces([]);
|
||||
setCities([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingProvinces(true);
|
||||
fetch("/api/locations/provinces", {
|
||||
headers: { "x-auth-token": normalizeToken(getToken()) },
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const rows = Array.isArray(data?.rows) ? data.rows : [];
|
||||
setProvinces(rows);
|
||||
|
||||
setWarehouse((prev) => {
|
||||
if (prev.provinceId || !prev.province) return prev;
|
||||
const matched = rows.find(
|
||||
(item: Province) =>
|
||||
item.name.trim().toLowerCase() === prev.province.trim().toLowerCase()
|
||||
);
|
||||
return matched ? { ...prev, provinceId: matched.id } : prev;
|
||||
});
|
||||
})
|
||||
.catch(() => setProvinces([]))
|
||||
.finally(() => setLoadingProvinces(false));
|
||||
}, [isIndonesia]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isIndonesia || !warehouse.provinceId) {
|
||||
setCities([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingCities(true);
|
||||
fetch(`/api/locations/cities?provinceId=${warehouse.provinceId}`, {
|
||||
headers: { "x-auth-token": normalizeToken(getToken()) },
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const rows = Array.isArray(data?.rows) ? data.rows : [];
|
||||
setCities(rows);
|
||||
|
||||
setWarehouse((prev) => {
|
||||
if (prev.cityId || !prev.city) return prev;
|
||||
const matched = rows.find(
|
||||
(item: City) =>
|
||||
item.name.trim().toLowerCase() === prev.city.trim().toLowerCase()
|
||||
);
|
||||
return matched ? { ...prev, cityId: matched.id } : prev;
|
||||
});
|
||||
})
|
||||
.catch(() => setCities([]))
|
||||
.finally(() => setLoadingCities(false));
|
||||
}, [isIndonesia, warehouse.provinceId]);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
@ -163,7 +241,7 @@ export default function StoreDetailPage() {
|
||||
postalCode: warehouse.postalCode.trim(),
|
||||
latitude: toNumber(warehouse.latitude),
|
||||
longitude: toNumber(warehouse.longitude),
|
||||
warehouseType: warehouse.warehouseType,
|
||||
warehouseType: "INA",
|
||||
},
|
||||
};
|
||||
|
||||
@ -394,9 +472,7 @@ export default function StoreDetailPage() {
|
||||
</label>
|
||||
<input
|
||||
value={warehouse.name}
|
||||
onChange={(e) =>
|
||||
setWarehouse((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
onChange={(e) => updateWarehouse({ name: e.target.value })}
|
||||
placeholder={sd.warehouseNamePlaceholder}
|
||||
className={headlineFieldClass}
|
||||
/>
|
||||
@ -408,9 +484,7 @@ export default function StoreDetailPage() {
|
||||
</label>
|
||||
<input
|
||||
value={warehouse.address}
|
||||
onChange={(e) =>
|
||||
setWarehouse((prev) => ({ ...prev, address: e.target.value }))
|
||||
}
|
||||
onChange={(e) => updateWarehouse({ address: e.target.value })}
|
||||
placeholder={sd.fullAddressPlaceholder}
|
||||
className={fieldClass}
|
||||
/>
|
||||
@ -420,39 +494,104 @@ export default function StoreDetailPage() {
|
||||
<label className="mb-1 block text-[10px] font-black uppercase tracking-widest text-outline">
|
||||
{sd.country}
|
||||
</label>
|
||||
<input
|
||||
<select
|
||||
value={warehouse.country}
|
||||
onChange={(e) =>
|
||||
setWarehouse((prev) => ({ ...prev, country: e.target.value }))
|
||||
updateWarehouse({
|
||||
country: e.target.value,
|
||||
province: "",
|
||||
provinceId: "",
|
||||
city: "",
|
||||
cityId: "",
|
||||
})
|
||||
}
|
||||
className={fieldClass}
|
||||
/>
|
||||
>
|
||||
<option value="">Pilih negara...</option>
|
||||
{COUNTRIES.map((country) => (
|
||||
<option key={country.code} value={country.name}>
|
||||
{country.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-[10px] font-black uppercase tracking-widest text-outline">
|
||||
{sd.province}
|
||||
</label>
|
||||
<input
|
||||
value={warehouse.province}
|
||||
onChange={(e) =>
|
||||
setWarehouse((prev) => ({ ...prev, province: e.target.value }))
|
||||
}
|
||||
className={fieldClass}
|
||||
/>
|
||||
{isIndonesia ? (
|
||||
<select
|
||||
value={warehouse.provinceId}
|
||||
onChange={(e) => {
|
||||
const selected = provinces.find((province) => province.id === e.target.value);
|
||||
updateWarehouse({
|
||||
provinceId: e.target.value,
|
||||
province: selected?.name || "",
|
||||
city: "",
|
||||
cityId: "",
|
||||
});
|
||||
}}
|
||||
disabled={loadingProvinces}
|
||||
className={fieldClass}
|
||||
>
|
||||
<option value="">
|
||||
{loadingProvinces ? "Memuat provinsi..." : "Pilih provinsi..."}
|
||||
</option>
|
||||
{provinces.map((province) => (
|
||||
<option key={province.id} value={province.id}>
|
||||
{province.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
value={warehouse.province}
|
||||
onChange={(e) => updateWarehouse({ province: e.target.value })}
|
||||
placeholder="Nama provinsi / state..."
|
||||
className={fieldClass}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-[10px] font-black uppercase tracking-widest text-outline">
|
||||
{sd.city}
|
||||
</label>
|
||||
<input
|
||||
value={warehouse.city}
|
||||
onChange={(e) =>
|
||||
setWarehouse((prev) => ({ ...prev, city: e.target.value }))
|
||||
}
|
||||
className={fieldClass}
|
||||
/>
|
||||
{isIndonesia ? (
|
||||
<select
|
||||
value={warehouse.cityId}
|
||||
onChange={(e) => {
|
||||
const selected = cities.find((city) => city.id === e.target.value);
|
||||
updateWarehouse({
|
||||
cityId: e.target.value,
|
||||
city: selected?.name || "",
|
||||
});
|
||||
}}
|
||||
disabled={!warehouse.provinceId || loadingCities}
|
||||
className={fieldClass}
|
||||
>
|
||||
<option value="">
|
||||
{!warehouse.provinceId
|
||||
? "Pilih provinsi dulu..."
|
||||
: loadingCities
|
||||
? "Memuat kota..."
|
||||
: "Pilih kota..."}
|
||||
</option>
|
||||
{cities.map((city) => (
|
||||
<option key={city.id} value={city.id}>
|
||||
{city.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
value={warehouse.city}
|
||||
onChange={(e) => updateWarehouse({ city: e.target.value })}
|
||||
placeholder="Nama kota..."
|
||||
className={fieldClass}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -461,12 +600,7 @@ export default function StoreDetailPage() {
|
||||
</label>
|
||||
<input
|
||||
value={warehouse.postalCode}
|
||||
onChange={(e) =>
|
||||
setWarehouse((prev) => ({
|
||||
...prev,
|
||||
postalCode: e.target.value,
|
||||
}))
|
||||
}
|
||||
onChange={(e) => updateWarehouse({ postalCode: e.target.value })}
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
@ -479,12 +613,7 @@ export default function StoreDetailPage() {
|
||||
type="number"
|
||||
step="any"
|
||||
value={warehouse.latitude}
|
||||
onChange={(e) =>
|
||||
setWarehouse((prev) => ({
|
||||
...prev,
|
||||
latitude: e.target.value,
|
||||
}))
|
||||
}
|
||||
onChange={(e) => updateWarehouse({ latitude: e.target.value })}
|
||||
placeholder="5.548290"
|
||||
className={fieldClass}
|
||||
/>
|
||||
@ -498,12 +627,7 @@ export default function StoreDetailPage() {
|
||||
type="number"
|
||||
step="any"
|
||||
value={warehouse.longitude}
|
||||
onChange={(e) =>
|
||||
setWarehouse((prev) => ({
|
||||
...prev,
|
||||
longitude: e.target.value,
|
||||
}))
|
||||
}
|
||||
onChange={(e) => updateWarehouse({ longitude: e.target.value })}
|
||||
placeholder="95.323753"
|
||||
className={fieldClass}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user