EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2
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";
8
9interface Supplier {
10 Spid: number; // Supplier ID
11 Name: string; // Name
12 Phone?: string; // Phone
13 Fax?: string; // Fax
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
22}
23
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();
33
34 useEffect(() => {
35 fetchSuppliers();
36 }, []);
37
38 useEffect(() => {
39 // Debounce search
40 const timer = setTimeout(() => {
41 if (searchTerm.trim().length > 0 || suppliers.length > 0) {
42 fetchSuppliers();
43 }
44 }, 300);
45
46 return () => clearTimeout(timer);
47 }, [searchTerm]);
48
49 const fetchSuppliers = async () => {
50 setLoading(true);
51 setError(null);
52 try {
53 // Use apiClient for type-safe API calls
54 const result = await apiClient.getSuppliers({
55 limit: 100,
56 search: searchTerm.trim() || undefined,
57 source: 'openapi'
58 });
59
60 // Handle OpenAPI response: { success, data: { data: { DATS: [...] } } }
61 if (result.success) {
62 const apiData = result.data as any;
63 const supplierData = apiData?.data?.DATS || [];
64 if (Array.isArray(supplierData) && supplierData.length > 0) {
65 // Apply sorting
66 let sortedSuppliers = [...supplierData];
67
68 switch (sortBy) {
69 case "name-az":
70 sortedSuppliers.sort((a, b) => (a.Name || "").localeCompare(b.Name || ""));
71 break;
72 case "name-za":
73 sortedSuppliers.sort((a, b) => (b.Name || "").localeCompare(a.Name || ""));
74 break;
75 case "location":
76 sortedSuppliers.sort((a, b) => (a.City || "").localeCompare(b.City || ""));
77 break;
78 }
79
80 setSuppliers(sortedSuppliers);
81 } else {
82 setError("No suppliers found");
83 setSuppliers([]);
84 }
85 } else {
86 setError("Failed to load suppliers");
87 setSuppliers([]);
88 }
89 } catch (error) {
90 console.error("Error fetching suppliers:", error);
91 setError("Network error while fetching suppliers");
92 setSuppliers([]);
93 } finally {
94 setLoading(false);
95 }
96 };
97
98 const getFullAddress = (supplier: Supplier): string => {
99 const parts = [];
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';
107 };
108
109 const openSupplierDetail = (supplier: Supplier) => {
110 setSelectedSupplier(supplier);
111 setShowDetailPopup(true);
112 };
113
114 const closePopup = () => {
115 setShowDetailPopup(false);
116 setSelectedSupplier(null);
117 };
118
119 const filteredSuppliers = suppliers.filter(supplier =>
120 searchTerm === "" ||
121 (supplier.Name || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
122 (supplier.Email || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
123 (supplier.City || "").toLowerCase().includes(searchTerm.toLowerCase())
124 );
125
126 return (
127 <div className="p-6">
128 {/* Header */}
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>
132 </div>
133
134 {/* Controls */}
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">
138 <input
139 type="text"
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"
142 value={searchTerm}
143 onChange={(e) => setSearchTerm(e.target.value)}
144 />
145 </div>
146 <div className="flex gap-2">
147 <select
148 className="p-3 border border-border rounded-lg focus:ring-2 focus:ring-brand"
149 value={sortBy}
150 onChange={(e) => setSortBy(e.target.value)}
151 >
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>
155 </select>
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
158 </button>
159 </div>
160 </div>
161
162 {/* Stats */}
163 <div className="flex gap-4 text-sm text-muted">
164 <span>Total suppliers: <strong>{suppliers.length}</strong></span>
165 {searchTerm && (
166 <span>Showing: <strong>{filteredSuppliers.length}</strong> results</span>
167 )}
168 </div>
169 </div>
170
171 {/* Error State */}
172 {error && (
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>
177 <button
178 onClick={fetchSuppliers}
179 className="ml-auto text-warn hover:text-warn/80 underline"
180 >
181 Retry
182 </button>
183 </div>
184 </div>
185 )}
186
187 {/* Loading State */}
188 {loading && (
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>
192 </div>
193 )}
194
195 {/* Supplier List */}
196 {!loading && (
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"}
203 </h3>
204 <p className="text-muted mb-4">
205 {searchTerm
206 ? `No suppliers match "${searchTerm}"`
207 : "Start by adding your first supplier"}
208 </p>
209 {!searchTerm && (
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
212 </button>
213 )}
214 </div>
215 ) : (
216 <div className="overflow-x-auto">
217 <table className="min-w-full">
218 <thead className="bg-surface-2">
219 <tr>
220 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
221 Supplier
222 </th>
223 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
224 Contact
225 </th>
226 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
227 Location
228 </th>
229 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
230 Actions
231 </th>
232 </tr>
233 </thead>
234 <tbody className="bg-surface divide-y divide-border">
235 {filteredSuppliers.map((supplier) => (
236 <tr
237 key={supplier.Spid}
238 onClick={() => router.push(`/pages/suppliers/${supplier.Spid}`)}
239 className="hover:bg-surface-2 cursor-pointer"
240 >
241 <td className="px-6 py-4 whitespace-nowrap">
242 <div>
243 <Link
244 href={`/pages/suppliers/${supplier.Spid}`}
245 className="text-sm font-medium text-brand hover:text-brand2 hover:underline"
246 onClick={(e) => e.stopPropagation()}
247 >
248 {supplier.Name || "Unnamed Supplier"}
249 </Link>
250 <div className="text-sm text-muted">
251 ID: {supplier.Spid}
252 </div>
253 </div>
254 </td>
255 <td className="px-6 py-4 whitespace-nowrap">
256 <div className="text-sm text-text">
257 {supplier.Phone && (
258 <div className="flex items-center gap-1"><Icon name="phone" size={14} /> {supplier.Phone}</div>
259 )}
260 {supplier.Email && (
261 <div className="flex items-center gap-1"><Icon name="email" size={14} /> {supplier.Email}</div>
262 )}
263 {!supplier.Phone && !supplier.Email && (
264 <span className="text-muted">No contact info</span>
265 )}
266 </div>
267 </td>
268 <td className="px-6 py-4">
269 <div className="text-sm text-text">
270 {supplier.City && (
271 <div>{supplier.City}</div>
272 )}
273 {supplier.State && supplier.State !== supplier.City && (
274 <div className="text-xs text-muted">{supplier.State}</div>
275 )}
276 {!supplier.City && (
277 <span className="text-muted">No location</span>
278 )}
279 </div>
280 </td>
281 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
282 <div className="flex gap-2">
283 <button
284 className="text-info hover:text-info/80 flex items-center gap-1"
285 onClick={(e) => {
286 e.stopPropagation();
287 openSupplierDetail(supplier);
288 }}
289 >
290 <Icon name="visibility" size={16} /> View
291 </button>
292 <button
293 className="text-success hover:text-success/80 flex items-center gap-1"
294 onClick={(e) => e.stopPropagation()}
295 >
296 <Icon name="shopping_cart" size={16} /> Order
297 </button>
298 <button
299 className="text-brand hover:text-brand2 flex items-center gap-1"
300 onClick={(e) => e.stopPropagation()}
301 >
302 <Icon name="edit" size={16} /> Edit
303 </button>
304 </div>
305 </td>
306 </tr>
307 ))}
308 </tbody>
309 </table>
310 </div>
311 )}
312 </div>
313 )}
314
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">
320 {/* Header */}
321 <div className="flex justify-between items-center mb-6">
322 <div>
323 <h2 className="text-2xl font-bold text-text">{selectedSupplier.Name}</h2>
324 <p className="text-muted">Supplier ID: {selectedSupplier.Spid}</p>
325 </div>
326 <button
327 onClick={closePopup}
328 className="text-muted hover:text-text text-2xl leading-none"
329 >
330 ×
331 </button>
332 </div>
333
334 {/* Contact Information */}
335 <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
336 <div>
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}
344 </a>
345 </div>
346 )}
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>
351 </div>
352 )}
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}
358 </a>
359 </div>
360 )}
361 </div>
362 </div>
363
364 <div>
365 <h3 className="text-lg font-semibold text-text mb-3">Address</h3>
366 <div className="text-text">
367 {getFullAddress(selectedSupplier)}
368 </div>
369 </div>
370 </div>
371
372 {/* Actions */}
373 <div className="flex justify-end gap-3">
374 <button
375 onClick={closePopup}
376 className="px-4 py-2 bg-surface-2 hover:bg-surface text-text rounded-md font-medium transition-colors"
377 >
378 Close
379 </button>
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
382 </button>
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
385 </button>
386 </div>
387 </div>
388 </div>
389 </div>
390 )}
391 </div>
392 );
393}