382 lines
13 KiB
TypeScript
382 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { COUNTRIES } from "@/lib/countries";
|
|
|
|
export interface WarehouseFormState {
|
|
name: string;
|
|
address: string;
|
|
country: string;
|
|
province: string;
|
|
provinceId: string;
|
|
city: string;
|
|
cityId: string;
|
|
postalCode: string;
|
|
latitude: string;
|
|
longitude: string;
|
|
warehouseType: string;
|
|
}
|
|
|
|
export const defaultWarehouseForm: WarehouseFormState = {
|
|
name: "",
|
|
address: "",
|
|
country: "Indonesia",
|
|
province: "",
|
|
provinceId: "",
|
|
city: "",
|
|
cityId: "",
|
|
postalCode: "",
|
|
latitude: "",
|
|
longitude: "",
|
|
warehouseType: "INA",
|
|
};
|
|
|
|
interface Province { id: string; name: string; }
|
|
interface City { id: string; name: string; }
|
|
|
|
function getToken() {
|
|
if (typeof window === "undefined") return "";
|
|
return sessionStorage.getItem("token") || localStorage.getItem("token") || "";
|
|
}
|
|
|
|
function normalizeToken(t: string) {
|
|
if (!t) return "";
|
|
return t.startsWith("Bearer ") ? t : `Bearer ${t}`;
|
|
}
|
|
|
|
const inputCls =
|
|
"w-full bg-surface-container-low border-b-2 border-outline/30 focus:border-primary border-t-0 border-x-0 rounded-t-lg px-4 py-3 text-sm font-medium text-on-surface placeholder:text-slate-400 focus:ring-0 focus:outline-none transition-all";
|
|
|
|
const labelCls = "block text-xs font-black uppercase tracking-[0.15em] text-slate-500 mb-2";
|
|
|
|
interface WarehouseFormProps {
|
|
initialData?: WarehouseFormState;
|
|
pageTitle: string;
|
|
pageSubtitle: string;
|
|
submitLabel: string;
|
|
submittingLabel: string;
|
|
successMessage: string;
|
|
apiMethod: "POST" | "PUT";
|
|
apiUrl: string;
|
|
}
|
|
|
|
export function WarehouseForm({
|
|
initialData,
|
|
pageTitle,
|
|
pageSubtitle,
|
|
submitLabel,
|
|
submittingLabel,
|
|
successMessage,
|
|
apiMethod,
|
|
apiUrl,
|
|
}: WarehouseFormProps) {
|
|
const router = useRouter();
|
|
const [form, setForm] = useState<WarehouseFormState>(initialData ?? defaultWarehouseForm);
|
|
const [provinces, setProvinces] = useState<Province[]>([]);
|
|
const [cities, setCities] = useState<City[]>([]);
|
|
const [loadingProvinces, setLoadingProvinces] = useState(false);
|
|
const [loadingCities, setLoadingCities] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [savingPhase, setSavingPhase] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
const isIndonesia = form.country === "Indonesia";
|
|
|
|
useEffect(() => {
|
|
if (initialData) setForm(initialData);
|
|
}, [initialData]);
|
|
|
|
useEffect(() => {
|
|
if (!isIndonesia) {
|
|
setProvinces([]);
|
|
setCities([]);
|
|
return;
|
|
}
|
|
setLoadingProvinces(true);
|
|
fetch("/api/locations/provinces", { headers: { "x-auth-token": normalizeToken(getToken()) } })
|
|
.then((r) => r.json())
|
|
.then((d) => setProvinces(Array.isArray(d?.rows) ? d.rows : []))
|
|
.catch(() => setProvinces([]))
|
|
.finally(() => setLoadingProvinces(false));
|
|
}, [isIndonesia]);
|
|
|
|
useEffect(() => {
|
|
if (!isIndonesia || !form.provinceId) {
|
|
setCities([]);
|
|
return;
|
|
}
|
|
setLoadingCities(true);
|
|
fetch(`/api/locations/cities?provinceId=${form.provinceId}`, {
|
|
headers: { "x-auth-token": normalizeToken(getToken()) },
|
|
})
|
|
.then((r) => r.json())
|
|
.then((d) => setCities(Array.isArray(d?.rows) ? d.rows : []))
|
|
.catch(() => setCities([]))
|
|
.finally(() => setLoadingCities(false));
|
|
}, [isIndonesia, form.provinceId]);
|
|
|
|
function update(patch: Partial<WarehouseFormState>) {
|
|
setForm((prev) => ({ ...prev, ...patch }));
|
|
}
|
|
|
|
async function handleSave() {
|
|
if (!form.address.trim()) { setError("Alamat wajib diisi"); return; }
|
|
|
|
setSaving(true);
|
|
setSavingPhase("Menyimpan perubahan...");
|
|
setError("");
|
|
|
|
const payload: Record<string, string | number | null> = {
|
|
name: form.name || null,
|
|
address: form.address,
|
|
country: form.country || null,
|
|
province: form.province || null,
|
|
city: form.city || null,
|
|
postalCode: form.postalCode || null,
|
|
latitude: form.latitude ? parseFloat(form.latitude) : null,
|
|
longitude: form.longitude ? parseFloat(form.longitude) : null,
|
|
warehouseType: form.warehouseType || null,
|
|
};
|
|
|
|
try {
|
|
const token = normalizeToken(getToken());
|
|
const res = await fetch(apiUrl, {
|
|
method: apiMethod,
|
|
headers: { "Content-Type": "application/json", "x-auth-token": token },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data?.responseDesc || data?.error || "Gagal menyimpan warehouse");
|
|
return;
|
|
}
|
|
setSuccess(true);
|
|
setTimeout(() => router.push("/dashboard/warehouse"), 1500);
|
|
} catch {
|
|
setError("Gagal terhubung ke server");
|
|
} finally {
|
|
setSaving(false);
|
|
setSavingPhase("");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="p-8 min-h-screen">
|
|
{/* Page Header */}
|
|
<div className="flex items-center justify-between mb-8">
|
|
<div>
|
|
<span className="text-primary font-bold tracking-widest text-xs uppercase">Management Dashboard</span>
|
|
<h1 className="text-4xl font-extrabold tracking-tight text-on-surface mt-1">{pageTitle}</h1>
|
|
<p className="text-slate-500 text-sm mt-1">{pageSubtitle}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => router.back()}
|
|
className="flex items-center gap-2 text-sm font-semibold text-slate-500 hover:text-primary transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined text-base">arrow_back</span>
|
|
Kembali
|
|
</button>
|
|
</div>
|
|
|
|
{/* Form Card */}
|
|
<div className="bg-white rounded-2xl border-l-4 border-primary shadow-sm p-8">
|
|
<h2 className="text-2xl font-bold tracking-tight mb-8">Facility Details</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Name */}
|
|
<div className="md:col-span-2">
|
|
<label className={labelCls}>Nama Warehouse</label>
|
|
<input
|
|
value={form.name}
|
|
onChange={(e) => update({ name: e.target.value })}
|
|
placeholder="Contoh: Gudang Utama Jakarta..."
|
|
className={inputCls}
|
|
/>
|
|
</div>
|
|
|
|
{/* Address */}
|
|
<div className="md:col-span-2">
|
|
<label className={labelCls}>Alamat *</label>
|
|
<input
|
|
value={form.address}
|
|
onChange={(e) => update({ address: e.target.value })}
|
|
placeholder="Jl. Nama Jalan No. ..."
|
|
className={inputCls}
|
|
/>
|
|
</div>
|
|
|
|
{/* Country */}
|
|
<div className="md:col-span-2">
|
|
<label className={labelCls}>Negara</label>
|
|
<select
|
|
value={form.country}
|
|
onChange={(e) =>
|
|
update({ country: e.target.value, province: "", provinceId: "", city: "", cityId: "" })
|
|
}
|
|
className={inputCls}
|
|
>
|
|
<option value="">Pilih negara...</option>
|
|
{COUNTRIES.map((c) => (
|
|
<option key={c.code} value={c.name}>{c.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Province */}
|
|
<div>
|
|
<label className={labelCls}>Provinsi / State</label>
|
|
{isIndonesia ? (
|
|
<select
|
|
value={form.provinceId}
|
|
onChange={(e) => {
|
|
const selected = provinces.find((p) => p.id === e.target.value);
|
|
update({ provinceId: e.target.value, province: selected?.name || "", city: "", cityId: "" });
|
|
}}
|
|
disabled={loadingProvinces}
|
|
className={inputCls}
|
|
>
|
|
<option value="">
|
|
{loadingProvinces ? "Memuat provinsi..." : "Pilih provinsi..."}
|
|
</option>
|
|
{provinces.map((p) => (
|
|
<option key={p.id} value={p.id}>{p.name}</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<input
|
|
value={form.province}
|
|
onChange={(e) => update({ province: e.target.value })}
|
|
placeholder="Nama provinsi / state..."
|
|
className={inputCls}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* City */}
|
|
<div>
|
|
<label className={labelCls}>Kota</label>
|
|
{isIndonesia ? (
|
|
<select
|
|
value={form.cityId}
|
|
onChange={(e) => {
|
|
const selected = cities.find((c) => c.id === e.target.value);
|
|
update({ cityId: e.target.value, city: selected?.name || "" });
|
|
}}
|
|
disabled={!form.provinceId || loadingCities}
|
|
className={inputCls}
|
|
>
|
|
<option value="">
|
|
{!form.provinceId ? "Pilih provinsi dulu..." : loadingCities ? "Memuat kota..." : "Pilih kota..."}
|
|
</option>
|
|
{cities.map((c) => (
|
|
<option key={c.id} value={c.id}>{c.name}</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<input
|
|
value={form.city}
|
|
onChange={(e) => update({ city: e.target.value })}
|
|
placeholder="Nama kota..."
|
|
className={inputCls}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Postal Code */}
|
|
<div>
|
|
<label className={labelCls}>Kode Pos</label>
|
|
<input
|
|
value={form.postalCode}
|
|
onChange={(e) => update({ postalCode: e.target.value })}
|
|
placeholder="Contoh: 13920"
|
|
className={inputCls}
|
|
/>
|
|
</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>
|
|
<input
|
|
type="number"
|
|
step="any"
|
|
value={form.latitude}
|
|
onChange={(e) => update({ latitude: e.target.value })}
|
|
placeholder="Contoh: -6.1891"
|
|
className={inputCls}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={labelCls}>Longitude</label>
|
|
<input
|
|
type="number"
|
|
step="any"
|
|
value={form.longitude}
|
|
onChange={(e) => update({ longitude: e.target.value })}
|
|
placeholder="Contoh: 106.9247"
|
|
className={inputCls}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status messages + buttons */}
|
|
<div className="mt-10 space-y-4">
|
|
{success && (
|
|
<div className="p-4 bg-tertiary-fixed text-on-tertiary-fixed-variant rounded-xl flex items-center gap-3 font-semibold text-sm">
|
|
<span className="material-symbols-outlined text-tertiary">check_circle</span>
|
|
{successMessage}
|
|
</div>
|
|
)}
|
|
{error && (
|
|
<div className="p-4 bg-error-container text-on-error-container rounded-xl flex items-center gap-3 font-semibold text-sm">
|
|
<span className="material-symbols-outlined">error</span>
|
|
{error}
|
|
</div>
|
|
)}
|
|
<div className="flex flex-col items-end gap-2">
|
|
{savingPhase && (
|
|
<p className="text-xs text-slate-400 flex items-center gap-1.5">
|
|
<span className="material-symbols-outlined text-sm animate-spin">progress_activity</span>
|
|
{savingPhase}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => router.back()}
|
|
disabled={saving}
|
|
className="px-6 py-3 rounded-xl border border-outline-variant/30 text-on-surface font-black text-sm uppercase tracking-[0.15em] hover:bg-surface-container-low transition-colors disabled:opacity-40"
|
|
>
|
|
Batal
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={saving || success}
|
|
className="bg-gradient-to-br from-primary to-primary-container text-white px-10 py-3.5 rounded-xl font-black text-sm uppercase tracking-[0.15em] shadow-xl shadow-primary/20 hover:scale-105 active:scale-95 transition-all disabled:opacity-60 disabled:hover:scale-100"
|
|
>
|
|
{saving ? (savingPhase || submittingLabel) : submitLabel}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|