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";
2import { useState, useEffect } from "react";
3import { apiClient } from "@/lib/client/apiClient";
4import { SkeletonRow } from "@/components/LoadingSpinner";
5import { Icon } from "@/contexts/IconContext";
6
7interface Customer {
8 Cid: number; // Customer ID
9 Name: string; // Name
10 Phone?: string; // Phone
11 Mobile?: string; // Mobile
12 Email?: string; // Email
13 Division?: string; // Company/Division
14 Key?: string; // Unique Key
15}
16
17export default function CustomersPage() {
18 const [searchTerm, setSearchTerm] = useState("");
19 const [sortBy, setSortBy] = useState("name-az");
20 const [customers, setCustomers] = useState<Customer[]>([]);
21 const [loading, setLoading] = useState(false);
22 const [showAddModal, setShowAddModal] = useState(false);
23 const [error, setError] = useState<string | null>(null);
24
25 useEffect(() => {
26 fetchCustomers();
27 }, []);
28
29 useEffect(() => {
30 // Debounce search
31 const timer = setTimeout(() => {
32 if (searchTerm.trim().length > 0 || customers.length > 0) {
33 fetchCustomers();
34 }
35 }, 300);
36
37 return () => clearTimeout(timer);
38 }, [searchTerm]);
39
40 const fetchCustomers = async () => {
41 setLoading(true);
42 setError(null);
43 try {
44 // Use apiClient for type-safe API calls
45 const result = await apiClient.getCustomers({
46 limit: 100,
47 search: searchTerm.trim() || undefined,
48 source: 'openapi'
49 });
50
51 // Handle OpenAPI response: { success, data: { data: { Customer: [...] } } }
52 if (result.success) {
53 const apiData = result.data as any;
54 const customerData = apiData?.data?.Customer || [];
55 if (Array.isArray(customerData) && customerData.length > 0) {
56 // Map field names to component interface
57 let sortedCustomers = customerData.map((c: any) => ({
58 Cid: c.Cid || c.f100,
59 Name: c.Name || c.f101 || 'Unnamed Customer',
60 Phone: c.Phone || c.f112,
61 Mobile: c.Mobile || c.f113,
62 Email: c.Email || c.f115,
63 Division: c.Division || c.f164,
64 Key: c.Key || c.f254
65 }));
66
67 switch (sortBy) {
68 case "name-az":
69 sortedCustomers.sort((a: Customer, b: Customer) => (a.Name || "").localeCompare(b.Name || ""));
70 break;
71 case "name-za":
72 sortedCustomers.sort((a: Customer, b: Customer) => (b.Name || "").localeCompare(a.Name || ""));
73 break;
74 case "recent":
75 // Sort by ID for now since we don't have last sale data in OpenAPI
76 sortedCustomers.sort((a: Customer, b: Customer) => (b.Cid || 0) - (a.Cid || 0));
77 break;
78 }
79
80 setCustomers(sortedCustomers);
81 } else {
82 setCustomers([]);
83 setError("No customer data available");
84 }
85 } else {
86 setCustomers([]);
87 setError("Failed to load customers");
88 }
89 } catch (error) {
90 console.error("Error fetching customers:", error);
91 setError("Network error while fetching customers");
92 // Use demo data as fallback
93 setCustomers([]);
94 } finally {
95 setLoading(false);
96 }
97 };
98
99 const filteredCustomers = customers.filter(customer => {
100 if (searchTerm === "") return true;
101
102 const searchLower = searchTerm.toLowerCase();
103 const name = (customer.Name || "").toLowerCase();
104 const company = (customer.Division || "").toLowerCase();
105 const email = (customer.Email || "").toLowerCase();
106 const phone = (customer.Phone || "").toLowerCase();
107 const mobile = (customer.Mobile || "").toLowerCase();
108
109 return name.includes(searchLower) ||
110 company.includes(searchLower) ||
111 email.includes(searchLower) ||
112 phone.includes(searchLower) ||
113 mobile.includes(searchLower);
114 });
115
116 return (
117 <div className="p-6">
118 {/* Header */}
119 <div className="mb-6">
120 <h1 className="text-3xl font-bold mb-2 flex items-center gap-2">
121 <Icon name="groups" size={32} className="text-brand" />
122 Customer Management
123 </h1>
124 <p className="text-muted">Manage customer records and contact information</p>
125 </div>
126
127 {/* Controls */}
128 <div className="bg-surface rounded-lg shadow p-6 mb-6">
129 <div className="flex flex-col sm:flex-row gap-4 mb-4">
130 <div className="flex-1">
131 <input
132 type="text"
133 placeholder="Search customers by name, company, or email..."
134 className="w-full p-3 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-brand"
135 value={searchTerm}
136 onChange={(e) => setSearchTerm(e.target.value)}
137 />
138 </div>
139 <div className="flex gap-2">
140 <select
141 className="p-3 border border-border rounded-lg focus:ring-2 focus:ring-brand"
142 value={sortBy}
143 onChange={(e) => setSortBy(e.target.value)}
144 >
145 <option value="name-az">Name A-Z</option>
146 <option value="name-za">Name Z-A</option>
147 <option value="recent">Recent Activity</option>
148 </select>
149 <button
150 className="px-4 py-3 bg-brand text-surface rounded-lg hover:bg-brand2 transition-colors flex items-center gap-2"
151 onClick={() => setShowAddModal(true)}
152 >
153 <Icon name="person_add" size={20} />
154 Add Customer
155 </button>
156 </div>
157 </div>
158
159 {/* Stats */}
160 <div className="flex gap-4 text-sm text-muted">
161 <span>Total customers: <strong>{customers.length}</strong></span>
162 {searchTerm && (
163 <span>Showing: <strong>{filteredCustomers.length}</strong> results</span>
164 )}
165 </div>
166 </div>
167
168 {/* Error State */}
169 {error && (
170 <div className="bg-warn/10 border border-warn/30 rounded-lg p-4 mb-6">
171 <div className="flex items-center">
172 <Icon name="warning" size={20} className="text-warn" />
173 <span className="ml-2 text-warn/90">{error}</span>
174 <button
175 onClick={fetchCustomers}
176 className="ml-auto text-warn hover:text-warn/80 underline"
177 >
178 Retry
179 </button>
180 </div>
181 </div>
182 )}
183
184 {/* Customer List */}
185 {!error && (
186 <div className="bg-surface rounded-lg shadow overflow-hidden">
187 {filteredCustomers.length === 0 ? (
188 <div className="p-8 text-center">
189 <Icon name="groups" size={64} className="text-muted mx-auto mb-4" />
190 <h3 className="text-lg font-medium text-text mb-2">
191 {searchTerm ? "No customers found" : "No customers yet"}
192 </h3>
193 <p className="text-muted mb-4">
194 {searchTerm
195 ? `No customers match "${searchTerm}"`
196 : "Start by adding your first customer"}
197 </p>
198 {!searchTerm && (
199 <button
200 className="px-4 py-2 bg-brand text-surface rounded-lg hover:bg-brand2 inline-flex items-center gap-2"
201 onClick={() => setShowAddModal(true)}
202 >
203 <Icon name="person_add" size={20} />
204 Add First Customer
205 </button>
206 )}
207 </div>
208 ) : (
209 <div className="overflow-x-auto">
210 <table className="min-w-full">
211 <thead className="bg-surface-2">
212 <tr>
213 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
214 Customer
215 </th>
216 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
217 Contact
218 </th>
219 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
220 Company
221 </th>
222 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
223 Actions
224 </th>
225 </tr>
226 </thead>
227 <tbody className="bg-surface divide-y divide-border">
228 {loading ? (
229 [...Array(5)].map((_, i) => <SkeletonRow key={i} />)
230 ) : (
231 filteredCustomers.map((customer) => (
232 <tr
233 key={customer.Cid}
234 className="hover:bg-surface-2 cursor-pointer transition-colors"
235 onClick={() => window.location.href = `/pages/customers/${customer.Cid}`}
236 >
237 <td className="px-6 py-4 whitespace-nowrap">
238 <div>
239 <div className="text-sm font-medium text-brand">
240 {customer.Name || "Unnamed Customer"}
241 </div>
242 <div className="text-sm text-muted">
243 ID: {customer.Cid}
244 </div>
245 </div>
246 </td>
247 <td className="px-6 py-4 whitespace-nowrap">
248 <div className="text-sm text-text">
249 {customer.Phone && (
250 <div className="flex items-center gap-1"><Icon name="phone" size={14} className="text-muted" /> {customer.Phone}</div>
251 )}
252 {customer.Mobile && (
253 <div className="flex items-center gap-1"><Icon name="smartphone" size={14} className="text-muted" /> {customer.Mobile}</div>
254 )}
255 {customer.Email && (
256 <div className="flex items-center gap-1"><Icon name="email" size={14} className="text-muted" /> {customer.Email}</div>
257 )}
258 {!customer.Phone && !customer.Mobile && !customer.Email && (
259 <span className="text-muted/60">No contact info</span>
260 )}
261 </div>
262 </td>
263 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
264 {customer.Division || "-"}
265 </td>
266 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
267 <div className="flex gap-2">
268 <button
269 onClick={(e) => {
270 e.stopPropagation();
271 window.location.href = `/pages/customers/${customer.Cid}`;
272 }}
273 className="text-brand hover:text-brand2 flex items-center gap-1"
274 >
275 <Icon name="visibility" size={16} />
276 View
277 </button>
278 <a
279 href={`/pages/customers/${customer.Cid}`}
280 onClick={(e) => e.stopPropagation()}
281 className="text-success hover:text-success/80 flex items-center gap-1"
282 >
283 <Icon name="edit" size={16} />
284 Edit
285 </a>
286 <button
287 className="text-brand2 hover:text-brand2/80 flex items-center gap-1"
288 onClick={() => window.location.href = `/pages/sales?customerId=${customer.Cid}`}
289 >
290 <Icon name="shopping_cart" size={16} />
291 Sale
292 </button>
293 <button
294 className="text-muted hover:text-text flex items-center gap-1"
295 onClick={() => window.location.href = `/pages/reports/sales-reports?customerId=${customer.Cid}`}
296 >
297 <Icon name="history" size={16} />
298 History
299 </button>
300 </div>
301 </td>
302 </tr>
303 ))
304 )}
305 </tbody>
306 </table>
307 </div>
308 )}
309 </div>
310 )}
311
312 {/* Add Customer Modal Placeholder */}
313 {showAddModal && (
314 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
315 <div className="bg-surface rounded-lg p-6 w-full max-w-md">
316 <h3 className="text-lg font-medium mb-4">Add New Customer</h3>
317 <div className="mb-4">
318 <label className="block text-sm font-medium text-text mb-1">
319 Customer Name
320 </label>
321 <input
322 type="text"
323 className="w-full p-2 border border-border rounded focus:ring-2 focus:ring-brand"
324 placeholder="Enter customer name"
325 />
326 </div>
327 <div className="flex justify-end gap-2">
328 <button
329 className="px-4 py-2 text-muted hover:text-text"
330 onClick={() => setShowAddModal(false)}
331 >
332 Cancel
333 </button>
334 <button
335 className="px-4 py-2 bg-brand text-surface rounded hover:bg-brand2 flex items-center gap-2"
336 onClick={() => {
337 setShowAddModal(false);
338 // TODO: Implement customer creation
339 alert("Customer creation coming soon!");
340 }}
341 >
342 <Icon name="save" size={16} />
343 Save
344 </button>
345 </div>
346 </div>
347 </div>
348 )}
349 </div>
350 );
351}