2import { useState, useEffect } from "react";
3import { apiClient } from "@/lib/client/apiClient";
4import { SkeletonRow } from "@/components/LoadingSpinner";
5import { Icon } from "@/contexts/IconContext";
8 Cid: number; // Customer ID
10 Phone?: string; // Phone
11 Mobile?: string; // Mobile
12 Email?: string; // Email
13 Division?: string; // Company/Division
14 Key?: string; // Unique Key
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);
31 const timer = setTimeout(() => {
32 if (searchTerm.trim().length > 0 || customers.length > 0) {
37 return () => clearTimeout(timer);
40 const fetchCustomers = async () => {
44 // Use apiClient for type-safe API calls
45 const result = await apiClient.getCustomers({
47 search: searchTerm.trim() || undefined,
51 // Handle OpenAPI response: { success, data: { data: { Customer: [...] } } }
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) => ({
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,
69 sortedCustomers.sort((a: Customer, b: Customer) => (a.Name || "").localeCompare(b.Name || ""));
72 sortedCustomers.sort((a: Customer, b: Customer) => (b.Name || "").localeCompare(a.Name || ""));
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));
80 setCustomers(sortedCustomers);
83 setError("No customer data available");
87 setError("Failed to load customers");
90 console.error("Error fetching customers:", error);
91 setError("Network error while fetching customers");
92 // Use demo data as fallback
99 const filteredCustomers = customers.filter(customer => {
100 if (searchTerm === "") return true;
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();
109 return name.includes(searchLower) ||
110 company.includes(searchLower) ||
111 email.includes(searchLower) ||
112 phone.includes(searchLower) ||
113 mobile.includes(searchLower);
117 <div className="p-6">
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" />
124 <p className="text-muted">Manage customer records and contact information</p>
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">
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"
136 onChange={(e) => setSearchTerm(e.target.value)}
139 <div className="flex gap-2">
141 className="p-3 border border-border rounded-lg focus:ring-2 focus:ring-brand"
143 onChange={(e) => setSortBy(e.target.value)}
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>
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)}
153 <Icon name="person_add" size={20} />
160 <div className="flex gap-4 text-sm text-muted">
161 <span>Total customers: <strong>{customers.length}</strong></span>
163 <span>Showing: <strong>{filteredCustomers.length}</strong> results</span>
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>
175 onClick={fetchCustomers}
176 className="ml-auto text-warn hover:text-warn/80 underline"
184 {/* Customer List */}
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"}
193 <p className="text-muted mb-4">
195 ? `No customers match "${searchTerm}"`
196 : "Start by adding your first customer"}
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)}
203 <Icon name="person_add" size={20} />
209 <div className="overflow-x-auto">
210 <table className="min-w-full">
211 <thead className="bg-surface-2">
213 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
216 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
219 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
222 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
227 <tbody className="bg-surface divide-y divide-border">
229 [...Array(5)].map((_, i) => <SkeletonRow key={i} />)
231 filteredCustomers.map((customer) => (
234 className="hover:bg-surface-2 cursor-pointer transition-colors"
235 onClick={() => window.location.href = `/pages/customers/${customer.Cid}`}
237 <td className="px-6 py-4 whitespace-nowrap">
239 <div className="text-sm font-medium text-brand">
240 {customer.Name || "Unnamed Customer"}
242 <div className="text-sm text-muted">
247 <td className="px-6 py-4 whitespace-nowrap">
248 <div className="text-sm text-text">
250 <div className="flex items-center gap-1"><Icon name="phone" size={14} className="text-muted" /> {customer.Phone}</div>
252 {customer.Mobile && (
253 <div className="flex items-center gap-1"><Icon name="smartphone" size={14} className="text-muted" /> {customer.Mobile}</div>
256 <div className="flex items-center gap-1"><Icon name="email" size={14} className="text-muted" /> {customer.Email}</div>
258 {!customer.Phone && !customer.Mobile && !customer.Email && (
259 <span className="text-muted/60">No contact info</span>
263 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
264 {customer.Division || "-"}
266 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
267 <div className="flex gap-2">
271 window.location.href = `/pages/customers/${customer.Cid}`;
273 className="text-brand hover:text-brand2 flex items-center gap-1"
275 <Icon name="visibility" size={16} />
279 href={`/pages/customers/${customer.Cid}`}
280 onClick={(e) => e.stopPropagation()}
281 className="text-success hover:text-success/80 flex items-center gap-1"
283 <Icon name="edit" size={16} />
287 className="text-brand2 hover:text-brand2/80 flex items-center gap-1"
288 onClick={() => window.location.href = `/pages/sales?customerId=${customer.Cid}`}
290 <Icon name="shopping_cart" size={16} />
294 className="text-muted hover:text-text flex items-center gap-1"
295 onClick={() => window.location.href = `/pages/reports/sales-reports?customerId=${customer.Cid}`}
297 <Icon name="history" size={16} />
312 {/* Add Customer Modal Placeholder */}
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">
323 className="w-full p-2 border border-border rounded focus:ring-2 focus:ring-brand"
324 placeholder="Enter customer name"
327 <div className="flex justify-end gap-2">
329 className="px-4 py-2 text-muted hover:text-text"
330 onClick={() => setShowAddModal(false)}
335 className="px-4 py-2 bg-brand text-surface rounded hover:bg-brand2 flex items-center gap-2"
337 setShowAddModal(false);
338 // TODO: Implement customer creation
339 alert("Customer creation coming soon!");
342 <Icon name="save" size={16} />