3import { useState, useEffect } from "react";
4import Link from "next/link";
5import { useRouter } from "next/navigation";
6import { apiClient } from "@/lib/client/apiClient";
7import { Icon } from "@/contexts/IconContext";
10 Spid: number; // Supplier ID
12 Phone?: string; // Phone
14 Email?: string; // Email
15 Address1?: string; // Address Line 1
16 Address2?: string; // Address Line 2
17 City?: string; // City
18 State?: string; // State
19 PostalCode?: string; // Postal Code
20 Country?: string; // Country
21 Key?: string; // Unique Key
24export default function SuppliersPage() {
25 const [searchTerm, setSearchTerm] = useState("");
26 const [sortBy, setSortBy] = useState("name-az");
27 const [suppliers, setSuppliers] = useState<Supplier[]>([]);
28 const [loading, setLoading] = useState(false);
29 const [selectedSupplier, setSelectedSupplier] = useState<Supplier | null>(null);
30 const [showDetailPopup, setShowDetailPopup] = useState(false);
31 const [error, setError] = useState<string | null>(null);
32 const router = useRouter();
40 const timer = setTimeout(() => {
41 if (searchTerm.trim().length > 0 || suppliers.length > 0) {
46 return () => clearTimeout(timer);
49 const fetchSuppliers = async () => {
53 // Use apiClient for type-safe API calls
54 const result = await apiClient.getSuppliers({
56 search: searchTerm.trim() || undefined,
60 // Handle OpenAPI response: { success, data: { data: { DATS: [...] } } }
62 const apiData = result.data as any;
63 const supplierData = apiData?.data?.DATS || [];
64 if (Array.isArray(supplierData) && supplierData.length > 0) {
66 let sortedSuppliers = [...supplierData];
70 sortedSuppliers.sort((a, b) => (a.Name || "").localeCompare(b.Name || ""));
73 sortedSuppliers.sort((a, b) => (b.Name || "").localeCompare(a.Name || ""));
76 sortedSuppliers.sort((a, b) => (a.City || "").localeCompare(b.City || ""));
80 setSuppliers(sortedSuppliers);
82 setError("No suppliers found");
86 setError("Failed to load suppliers");
90 console.error("Error fetching suppliers:", error);
91 setError("Network error while fetching suppliers");
98 const getFullAddress = (supplier: Supplier): string => {
100 if (supplier.Address1) parts.push(supplier.Address1);
101 if (supplier.Address2) parts.push(supplier.Address2);
102 if (supplier.City) parts.push(supplier.City);
103 if (supplier.State) parts.push(supplier.State);
104 if (supplier.PostalCode) parts.push(supplier.PostalCode);
105 if (supplier.Country) parts.push(supplier.Country);
106 return parts.join(', ') || 'No address available';
109 const openSupplierDetail = (supplier: Supplier) => {
110 setSelectedSupplier(supplier);
111 setShowDetailPopup(true);
114 const closePopup = () => {
115 setShowDetailPopup(false);
116 setSelectedSupplier(null);
119 const filteredSuppliers = suppliers.filter(supplier =>
121 (supplier.Name || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
122 (supplier.Email || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
123 (supplier.City || "").toLowerCase().includes(searchTerm.toLowerCase())
127 <div className="p-6">
129 <div className="mb-6">
130 <h1 className="text-3xl font-bold mb-2 flex items-center gap-2"><Icon name="local_shipping" size={32} /> Supplier Management</h1>
131 <p className="text-muted">Manage supplier records and vendor information</p>
135 <div className="bg-surface rounded-lg shadow p-6 mb-6">
136 <div className="flex flex-col sm:flex-row gap-4 mb-4">
137 <div className="flex-1">
140 placeholder="Search suppliers by name, email, or location..."
141 className="w-full p-3 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-brand"
143 onChange={(e) => setSearchTerm(e.target.value)}
146 <div className="flex gap-2">
148 className="p-3 border border-border rounded-lg focus:ring-2 focus:ring-brand"
150 onChange={(e) => setSortBy(e.target.value)}
152 <option value="name-az">Name A-Z</option>
153 <option value="name-za">Name Z-A</option>
154 <option value="location">By Location</option>
156 <button className="px-4 py-3 bg-success text-surface rounded-lg hover:bg-success/80 transition-colors flex items-center gap-2">
157 <Icon name="add" size={18} /> Add Supplier
163 <div className="flex gap-4 text-sm text-muted">
164 <span>Total suppliers: <strong>{suppliers.length}</strong></span>
166 <span>Showing: <strong>{filteredSuppliers.length}</strong> results</span>
173 <div className="bg-warn/10 border border-warn/30 rounded-lg p-4 mb-6">
174 <div className="flex items-center">
175 <Icon name="warning" className="text-warn" size={20} />
176 <span className="ml-2 text-warn">{error}</span>
178 onClick={fetchSuppliers}
179 className="ml-auto text-warn hover:text-warn/80 underline"
187 {/* Loading State */}
189 <div className="bg-surface rounded-lg shadow p-8 text-center">
190 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-brand"></div>
191 <p className="mt-2 text-muted">Loading suppliers...</p>
195 {/* Supplier List */}
197 <div className="bg-surface rounded-lg shadow overflow-hidden">
198 {filteredSuppliers.length === 0 ? (
199 <div className="p-8 text-center">
200 <div className="text-6xl mb-4"><Icon name="local_shipping" size={72} /></div>
201 <h3 className="text-lg font-medium text-text mb-2">
202 {searchTerm ? "No suppliers found" : "No suppliers yet"}
204 <p className="text-muted mb-4">
206 ? `No suppliers match "${searchTerm}"`
207 : "Start by adding your first supplier"}
210 <button className="px-4 py-2 bg-success text-surface rounded-lg hover:bg-success/80 flex items-center gap-2 mx-auto">
211 <Icon name="add" size={18} /> Add First Supplier
216 <div className="overflow-x-auto">
217 <table className="min-w-full">
218 <thead className="bg-surface-2">
220 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
223 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
226 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
229 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
234 <tbody className="bg-surface divide-y divide-border">
235 {filteredSuppliers.map((supplier) => (
238 onClick={() => router.push(`/pages/suppliers/${supplier.Spid}`)}
239 className="hover:bg-surface-2 cursor-pointer"
241 <td className="px-6 py-4 whitespace-nowrap">
244 href={`/pages/suppliers/${supplier.Spid}`}
245 className="text-sm font-medium text-brand hover:text-brand2 hover:underline"
246 onClick={(e) => e.stopPropagation()}
248 {supplier.Name || "Unnamed Supplier"}
250 <div className="text-sm text-muted">
255 <td className="px-6 py-4 whitespace-nowrap">
256 <div className="text-sm text-text">
258 <div className="flex items-center gap-1"><Icon name="phone" size={14} /> {supplier.Phone}</div>
261 <div className="flex items-center gap-1"><Icon name="email" size={14} /> {supplier.Email}</div>
263 {!supplier.Phone && !supplier.Email && (
264 <span className="text-muted">No contact info</span>
268 <td className="px-6 py-4">
269 <div className="text-sm text-text">
271 <div>{supplier.City}</div>
273 {supplier.State && supplier.State !== supplier.City && (
274 <div className="text-xs text-muted">{supplier.State}</div>
277 <span className="text-muted">No location</span>
281 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
282 <div className="flex gap-2">
284 className="text-info hover:text-info/80 flex items-center gap-1"
287 openSupplierDetail(supplier);
290 <Icon name="visibility" size={16} /> View
293 className="text-success hover:text-success/80 flex items-center gap-1"
294 onClick={(e) => e.stopPropagation()}
296 <Icon name="shopping_cart" size={16} /> Order
299 className="text-brand hover:text-brand2 flex items-center gap-1"
300 onClick={(e) => e.stopPropagation()}
302 <Icon name="edit" size={16} /> Edit
315 {/* Detail Popup Modal */}
316 {showDetailPopup && selectedSupplier && (
317 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
318 <div className="bg-surface rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
319 <div className="p-6">
321 <div className="flex justify-between items-center mb-6">
323 <h2 className="text-2xl font-bold text-text">{selectedSupplier.Name}</h2>
324 <p className="text-muted">Supplier ID: {selectedSupplier.Spid}</p>
328 className="text-muted hover:text-text text-2xl leading-none"
334 {/* Contact Information */}
335 <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
337 <h3 className="text-lg font-semibold text-text mb-3">Contact Information</h3>
338 <div className="space-y-2">
339 {selectedSupplier.Phone && (
340 <div className="flex items-center gap-2">
341 <Icon name="phone" size={18} className="text-muted" />
342 <a href={`tel:${selectedSupplier.Phone}`} className="text-brand hover:text-brand2">
343 {selectedSupplier.Phone}
347 {selectedSupplier.Fax && (
348 <div className="flex items-center gap-2">
349 <Icon name="print" size={18} className="text-muted" />
350 <span>{selectedSupplier.Fax}</span>
353 {selectedSupplier.Email && (
354 <div className="flex items-center gap-2">
355 <Icon name="email" size={18} className="text-muted" />
356 <a href={`mailto:${selectedSupplier.Email}`} className="text-brand hover:text-brand2">
357 {selectedSupplier.Email}
365 <h3 className="text-lg font-semibold text-text mb-3">Address</h3>
366 <div className="text-text">
367 {getFullAddress(selectedSupplier)}
373 <div className="flex justify-end gap-3">
376 className="px-4 py-2 bg-surface-2 hover:bg-surface text-text rounded-md font-medium transition-colors"
380 <button className="px-4 py-2 bg-info hover:bg-info/80 text-surface rounded-md font-medium transition-colors flex items-center gap-2">
381 <Icon name="edit" size={18} /> Edit Supplier
383 <button className="px-4 py-2 bg-success hover:bg-success/80 text-surface rounded-md font-medium transition-colors flex items-center gap-2">
384 <Icon name="shopping_cart" size={18} /> Create Order