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 { useEffect, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5import { Icon } from '@/contexts/IconContext';
6
7interface Store {
8 f100: number; // ID
9 f101: string; // Name
10 f102?: number; // Latitude
11 f103?: number; // Longitude
12 f150?: string; // Phone
13 f151?: string; // Email
14 f164?: string; // Store Type
15 f185?: number; // Config Type
16 f186?: number; // Parent Location
17}
18
19interface StoreFormData {
20 id?: string;
21 name: string;
22 phone: string;
23 email: string;
24 isNew: boolean;
25}
26
27export default function StoresPage() {
28 const [stores, setStores] = useState<Store[]>([]);
29 const [filteredStores, setFilteredStores] = useState<Store[]>([]);
30 const [loading, setLoading] = useState(true);
31 const [searchTerm, setSearchTerm] = useState("");
32 const [showModal, setShowModal] = useState(false);
33 const [formData, setFormData] = useState<StoreFormData>({
34 name: "",
35 phone: "",
36 email: "",
37 isNew: true,
38 });
39
40 useEffect(() => {
41 loadStores();
42 }, []);
43
44 useEffect(() => {
45 applyFilters();
46 }, [stores, searchTerm]);
47
48 const loadStores = async () => {
49 try {
50 setLoading(true);
51 const result = await apiClient.getLocations({ source: 'elink' });
52
53 if (result.success && (result.data as any)?.DATS) {
54 setStores((result.data as any).DATS);
55 }
56 } catch (error) {
57 console.error("Error loading stores:", error);
58 } finally {
59 setLoading(false);
60 }
61 };
62
63 const applyFilters = () => {
64 if (!searchTerm) {
65 setFilteredStores(stores);
66 return;
67 }
68
69 const search = searchTerm.toLowerCase();
70 const filtered = stores.filter((store) => {
71 return (
72 store.f100?.toString().includes(search) ||
73 store.f101?.toLowerCase().includes(search) ||
74 store.f150?.toLowerCase().includes(search) ||
75 store.f151?.toLowerCase().includes(search)
76 );
77 });
78
79 setFilteredStores(filtered);
80 };
81
82 const openNewStoreModal = () => {
83 setFormData({
84 name: "",
85 phone: "",
86 email: "",
87 isNew: true,
88 });
89 setShowModal(true);
90 };
91
92 const openEditModal = (store: Store) => {
93 setFormData({
94 id: store.f100.toString(),
95 name: store.f101 || "",
96 phone: store.f150 || "",
97 email: store.f151 || "",
98 isNew: false,
99 });
100 setShowModal(true);
101 };
102
103 const handleSave = async () => {
104 try {
105 // Validate required fields
106 if (!formData.name) {
107 alert("Store name is required");
108 return;
109 }
110
111 const locationData = {
112 id: formData.id ? parseInt(formData.id) : undefined,
113 name: formData.name,
114 phone: formData.phone || undefined,
115 email: formData.email || undefined,
116 };
117
118 const result = await apiClient.saveLocation(locationData);
119
120 if (result.success) {
121 setShowModal(false);
122 loadStores();
123 alert(formData.isNew ? "Store created successfully" : "Store updated successfully");
124 } else {
125 alert(result.error || "Failed to save store");
126 }
127 } catch (error) {
128 console.error("Error saving store:", error);
129 alert("Failed to save store");
130 }
131 };
132
133 const exportToExcel = () => {
134 // Create CSV content (Excel will open CSV files)
135 const headers = ['ID', 'Name', 'Phone', 'Email', 'Latitude', 'Longitude', 'Store Type'];
136 const csvRows = [
137 headers.join(','),
138 ...stores.map(store => [
139 store.f100,
140 `"${(store.f101 || '').replace(/"/g, '""')}"`,
141 `"${(store.f150 || '').replace(/"/g, '""')}"`,
142 `"${(store.f151 || '').replace(/"/g, '""')}"`,
143 store.f102 || '',
144 store.f103 || '',
145 `"${(store.f164 || '').replace(/"/g, '""')}"`,
146 ].join(','))
147 ];
148
149 const csvContent = csvRows.join('\n');
150 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
151 const link = document.createElement('a');
152 const url = URL.createObjectURL(blob);
153
154 link.setAttribute('href', url);
155 link.setAttribute('download', `Stores_${new Date().toISOString().split('T')[0]}.csv`);
156 link.style.visibility = 'hidden';
157 document.body.appendChild(link);
158 link.click();
159 document.body.removeChild(link);
160 };
161
162 if (loading) {
163 return (
164 <div className="p-6 min-h-screen">
165 <div className="animate-pulse space-y-4">
166 <div className="h-8 bg-surface-2 rounded w-1/4"></div>
167 <div className="h-64 bg-surface-2 rounded"></div>
168 </div>
169 </div>
170 );
171 }
172
173 return (
174 <div className="p-6 min-h-screen">
175 {/* Header */}
176 <div className="mb-6">
177 <h1 className="text-3xl font-bold text-text mb-2 flex items-center gap-3">
178 <Icon name="store" size={32} />
179 Stores Management
180 </h1>
181 <p className="text-muted">
182 Manage store locations, settings, and configurations
183 </p>
184 </div>
185
186 {/* Action Cards */}
187 <div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6">
188 <button
189 onClick={openNewStoreModal}
190 className="bg-surface rounded-lg shadow p-6 hover:shadow-lg transition-shadow flex flex-col items-center justify-center gap-3 border-2 border-transparent hover:border-brand"
191 >
192 <Icon name="add_business" size={48} className="text-brand" />
193 <span className="font-semibold text-text">New Store</span>
194 </button>
195
196 <a
197 href="/pages/settings/stores/topology"
198 className="bg-surface rounded-lg shadow p-6 hover:shadow-lg transition-shadow flex flex-col items-center justify-center gap-3 border-2 border-transparent hover:border-brand"
199 >
200 <Icon name="hub" size={48} className="text-brand" />
201 <span className="font-semibold text-text">Topology</span>
202 </a>
203
204 <button
205 onClick={exportToExcel}
206 className="bg-surface rounded-lg shadow p-6 hover:shadow-lg transition-shadow flex flex-col items-center justify-center gap-3 border-2 border-transparent hover:border-brand"
207 >
208 <Icon name="file_download" size={48} className="text-brand" />
209 <span className="font-semibold text-text">Excel Export</span>
210 </button>
211 </div>
212
213 {/* Search Bar */}
214 <div className="bg-surface rounded-lg shadow p-4 mb-6">
215 <div className="flex items-center gap-4">
216 <div className="flex-1">
217 <input
218 type="text"
219 placeholder="Search stores..."
220 value={searchTerm}
221 onChange={(e) => setSearchTerm(e.target.value)}
222 className="w-full px-4 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent"
223 />
224 </div>
225 <div className="text-sm text-muted">
226 Displaying {filteredStores.length} of {stores.length} stores
227 </div>
228 </div>
229 </div>
230
231 {/* Stores Table */}
232 <div className="bg-surface rounded-lg shadow overflow-hidden">
233 <div className="overflow-x-auto">
234 <table className="w-full">
235 <thead className="bg-[var(--brand)] text-surface">
236 <tr>
237 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
238 ID
239 </th>
240 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
241 Name
242 </th>
243 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
244 Phone
245 </th>
246 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
247 Email
248 </th>
249 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
250 Actions
251 </th>
252 </tr>
253 </thead>
254 <tbody className="bg-surface divide-y divide-border">
255 {filteredStores.map((store) => (
256 <tr key={store.f100} className="hover:bg-surface-2">
257 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
258 {store.f100}
259 </td>
260 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text">
261 {store.f101}
262 </td>
263 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
264 {store.f150 || "—"}
265 </td>
266 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
267 {store.f151 ? (
268 <a
269 href={`mailto:${store.f151}`}
270 className="text-brand hover:underline flex items-center gap-1"
271 >
272 <Icon name="email" size={16} />
273 {store.f151}
274 </a>
275 ) : (
276 "—"
277 )}
278 </td>
279 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
280 <button
281 onClick={() => openEditModal(store)}
282 className="text-brand hover:text-brand2"
283 >
284 Quick Edit
285 </button>
286 <a
287 href={`/pages/settings/stores/${store.f100}`}
288 className="text-brand hover:text-brand2"
289 >
290 Full Edit
291 </a>
292 </td>
293 </tr>
294 ))}
295 </tbody>
296 </table>
297 </div>
298 </div>
299
300 {/* Edit/Create Modal */}
301 {showModal && (
302 <div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4 overflow-y-auto">
303 <div className="bg-surface/95 backdrop-blur-md rounded-lg shadow-xl max-w-2xl w-full my-8">
304 <div className="bg-brand/90 backdrop-blur-sm text-white px-6 py-4 rounded-t-lg flex items-center justify-between">
305 <h2 className="text-2xl font-bold text-white">
306 {formData.isNew ? "Create New Store" : "Edit Store"}
307 </h2>
308 <button
309 onClick={() => setShowModal(false)}
310 className="text-white hover:text-white/80"
311 >
312 <Icon name="close" size={24} />
313 </button>
314 </div>
315
316 <div className="p-6 bg-surface/95 backdrop-blur-md rounded-b-lg">
317 <div className="space-y-4">
318 {formData.isNew && (
319 <div>
320 <label className="block text-sm font-medium text-text mb-1">
321 Location ID
322 </label>
323 <input
324 type="text"
325 value={formData.id || ""}
326 onChange={(e) => setFormData({ ...formData, id: e.target.value })}
327 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
328 placeholder="Leave blank for auto-assign"
329 />
330 <p className="text-xs text-muted mt-1">Rarely used, generally left blank</p>
331 </div>
332 )}
333
334 <div>
335 <label className="block text-sm font-medium text-text mb-1">
336 Store Name *
337 </label>
338 <input
339 type="text"
340 value={formData.name}
341 onChange={(e) => setFormData({ ...formData, name: e.target.value })}
342 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
343 required
344 />
345 <p className="text-xs text-muted mt-1">Used as name of the store on your reports</p>
346 </div>
347
348 <div>
349 <label className="block text-sm font-medium text-text mb-1">
350 Phone
351 </label>
352 <input
353 type="tel"
354 value={formData.phone}
355 onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
356 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
357 />
358 </div>
359
360 <div>
361 <label className="block text-sm font-medium text-text mb-1">
362 Email
363 </label>
364 <input
365 type="email"
366 value={formData.email}
367 onChange={(e) => setFormData({ ...formData, email: e.target.value })}
368 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
369 />
370 </div>
371 </div>
372
373 <div className="mt-6 flex justify-end gap-3">
374 <button
375 onClick={() => setShowModal(false)}
376 className="px-6 py-2 border border-border rounded-lg hover:bg-surface-2 transition-colors"
377 >
378 Cancel
379 </button>
380 <button
381 onClick={handleSave}
382 className="bg-[#00543b] text-white px-6 py-2 rounded-lg hover:bg-[#003d2b] transition-colors"
383 >
384 Save
385 </button>
386 </div>
387 </div>
388 </div>
389 </div>
390 )}
391 </div>
392 );
393}