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";
5
6interface Employee {
7 f100: number; // ID
8 f101: string; // Short Name
9 f102: string; // Full Name
10 f103?: string; // POS Login ID
11 f104?: string; // Start Date
12 f105?: string; // End Date
13 f106?: string; // Main Location
14 f107?: string; // Job Title
15 f108?: string; // Job Code
16 f109?: string; // Department ID
17 f110?: string; // External ID
18 f114?: string; // NZTA UID
19}
20
21interface EmployeeFormData {
22 id?: number;
23 shortName: string;
24 fullName: string;
25 posLoginId: string;
26 startDate: string;
27 endDate: string;
28 mainLocation: string;
29 jobTitle: string;
30 jobCode: string;
31 departmentId: string;
32 externalId: string;
33 nztaUid: string;
34}
35
36export default function EmployeesPage() {
37 const [employees, setEmployees] = useState<Employee[]>([]);
38 const [filteredEmployees, setFilteredEmployees] = useState<Employee[]>([]);
39 const [loading, setLoading] = useState(true);
40 const [searchTerm, setSearchTerm] = useState("");
41 const [showModal, setShowModal] = useState(false);
42 const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
43 const [formData, setFormData] = useState<EmployeeFormData>({
44 shortName: "",
45 fullName: "",
46 posLoginId: "",
47 startDate: "",
48 endDate: "",
49 mainLocation: "",
50 jobTitle: "",
51 jobCode: "",
52 departmentId: "",
53 externalId: "",
54 nztaUid: "",
55 });
56
57 useEffect(() => {
58 loadEmployees();
59 }, []);
60
61 useEffect(() => {
62 applyFilters();
63 }, [employees, searchTerm]);
64
65 const loadEmployees = async () => {
66 try {
67 setLoading(true);
68 const result = await apiClient.getEmployees();
69
70 if (result.success && result.data?.DATS) {
71 setEmployees(result.data.DATS);
72 }
73 } catch (error) {
74 console.error("Error loading employees:", error);
75 } finally {
76 setLoading(false);
77 }
78 };
79
80 const applyFilters = () => {
81 if (!searchTerm) {
82 setFilteredEmployees(employees);
83 return;
84 }
85
86 const search = searchTerm.toLowerCase();
87 const filtered = employees.filter((emp) => {
88 return (
89 emp.f100?.toString().includes(search) ||
90 emp.f101?.toLowerCase().includes(search) ||
91 emp.f102?.toLowerCase().includes(search) ||
92 emp.f107?.toLowerCase().includes(search) ||
93 emp.f110?.toLowerCase().includes(search)
94 );
95 });
96
97 setFilteredEmployees(filtered);
98 };
99
100 const openNewEmployeeModal = () => {
101 setEditingEmployee(null);
102 setFormData({
103 shortName: "",
104 fullName: "",
105 posLoginId: "",
106 startDate: "",
107 endDate: "",
108 mainLocation: "",
109 jobTitle: "",
110 jobCode: "",
111 departmentId: "",
112 externalId: "",
113 nztaUid: "",
114 });
115 setShowModal(true);
116 };
117
118 const openEditModal = (employee: Employee) => {
119 setEditingEmployee(employee);
120 setFormData({
121 id: employee.f100,
122 shortName: employee.f101 || "",
123 fullName: employee.f102 || "",
124 posLoginId: employee.f103 || "",
125 startDate: employee.f104 || "",
126 endDate: employee.f105 || "",
127 mainLocation: employee.f106 || "",
128 jobTitle: employee.f107 || "",
129 jobCode: employee.f108 || "",
130 departmentId: employee.f109 || "",
131 externalId: employee.f110 || "",
132 nztaUid: employee.f114 || "",
133 });
134 setShowModal(true);
135 };
136
137 const handleSave = async () => {
138 try {
139 const result = await apiClient.saveEmployee(formData);
140
141 if (result.success) {
142 setShowModal(false);
143 loadEmployees();
144 } else {
145 alert("Failed to save employee: " + (result.error || "Unknown error"));
146 }
147 } catch (error) {
148 console.error("Error saving employee:", error);
149 alert("Failed to save employee");
150 }
151 };
152
153 const formatDate = (dateStr: string) => {
154 if (!dateStr) return "—";
155 const date = new Date(dateStr);
156 return date.toLocaleDateString("en-GB", {
157 day: "2-digit",
158 month: "short",
159 year: "numeric",
160 });
161 };
162
163 if (loading) {
164 return (
165 <div className="p-6 bg-surface-2 min-h-screen">
166 <div className="animate-pulse space-y-4">
167 <div className="h-8 bg-surface-2 rounded w-1/4"></div>
168 <div className="h-64 bg-surface-2 rounded"></div>
169 </div>
170 </div>
171 );
172 }
173
174 return (
175 <div className="p-6 bg-surface-2 min-h-screen">
176 {/* Header */}
177 <div className="mb-6">
178 <h1 className="text-3xl font-bold text-text mb-2">
179 Employee Management
180 </h1>
181 <p className="text-muted">
182 Employees are people who work for you and are known to the POS, but do not
183 necessarily have access to the Point of Sale. Often used for commission tracking
184 for non-retail staff.
185 </p>
186 </div>
187
188 {/* Action Bar */}
189 <div className="bg-surface rounded-lg shadow p-4 mb-6">
190 <div className="flex flex-wrap items-center gap-4">
191 <button
192 onClick={openNewEmployeeModal}
193 className="bg-[#00543b] text-white px-6 py-2 rounded-lg hover:bg-[#003d2b] transition-colors flex items-center gap-2"
194 >
195 <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
196 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
197 </svg>
198 Add New Employee
199 </button>
200
201 <div className="flex-1 min-w-[200px]">
202 <input
203 type="text"
204 placeholder="Search employees..."
205 value={searchTerm}
206 onChange={(e) => setSearchTerm(e.target.value)}
207 className="w-full px-4 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
208 />
209 </div>
210 </div>
211
212 <div className="mt-2 text-sm text-muted">
213 Displaying {filteredEmployees.length} of {employees.length} employees
214 </div>
215 </div>
216
217 {/* Employees Table */}
218 <div className="bg-surface rounded-lg shadow overflow-hidden">
219 <div className="overflow-x-auto">
220 <table className="w-full">
221 <thead className="bg-[var(--brand)] text-surface">
222 <tr>
223 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
224 ID
225 </th>
226 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
227 Short Name
228 </th>
229 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
230 Full Name
231 </th>
232 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
233 Job Title
234 </th>
235 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
236 Start Date
237 </th>
238 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
239 End Date
240 </th>
241 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
242 Actions
243 </th>
244 </tr>
245 </thead>
246 <tbody className="bg-surface divide-y divide-gray-200">
247 {filteredEmployees.map((employee) => (
248 <tr key={employee.f100} className="hover:bg-surface-2">
249 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
250 {employee.f100}
251 </td>
252 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text">
253 {employee.f101}
254 </td>
255 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
256 {employee.f102}
257 </td>
258 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
259 {employee.f107 || "—"}
260 </td>
261 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
262 {formatDate(employee.f104 || "")}
263 </td>
264 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
265 {formatDate(employee.f105 || "")}
266 </td>
267 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
268 <button
269 onClick={() => openEditModal(employee)}
270 className="text-[#00543b] hover:text-[#003d2b]"
271 >
272 Edit
273 </button>
274 </td>
275 </tr>
276 ))}
277 </tbody>
278 </table>
279 </div>
280 </div>
281
282 {/* Edit/Create Modal */}
283 {showModal && (
284 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-y-auto">
285 <div className="bg-surface rounded-lg shadow-xl max-w-3xl w-full my-8">
286 <div className="bg-[#00543b] text-white px-6 py-4 rounded-t-lg flex items-center justify-between">
287 <h2 className="text-2xl font-bold">
288 {editingEmployee ? "Edit Employee" : "New Employee"}
289 </h2>
290 <button
291 onClick={() => setShowModal(false)}
292 className="text-white hover:text-muted/70"
293 >
294 <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
295 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
296 </svg>
297 </button>
298 </div>
299
300 <div className="p-6 max-h-[70vh] overflow-y-auto">
301 <div className="grid grid-cols-2 gap-6">
302 <div>
303 <label className="block text-sm font-medium text-text mb-1">
304 Short Name *
305 </label>
306 <input
307 type="text"
308 value={formData.shortName}
309 onChange={(e) => setFormData({ ...formData, shortName: e.target.value })}
310 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
311 required
312 />
313 </div>
314
315 <div>
316 <label className="block text-sm font-medium text-text mb-1">
317 Full Name *
318 </label>
319 <input
320 type="text"
321 value={formData.fullName}
322 onChange={(e) => setFormData({ ...formData, fullName: e.target.value })}
323 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
324 required
325 />
326 </div>
327
328 <div>
329 <label className="block text-sm font-medium text-text mb-1">
330 POS Login ID
331 </label>
332 <input
333 type="text"
334 value={formData.posLoginId}
335 onChange={(e) => setFormData({ ...formData, posLoginId: e.target.value })}
336 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
337 />
338 <p className="text-xs text-muted mt-1">Links to staff login account</p>
339 </div>
340
341 <div>
342 <label className="block text-sm font-medium text-text mb-1">
343 External ID
344 </label>
345 <input
346 type="text"
347 value={formData.externalId}
348 onChange={(e) => setFormData({ ...formData, externalId: e.target.value })}
349 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
350 />
351 <p className="text-xs text-muted mt-1">External system reference</p>
352 </div>
353
354 <div>
355 <label className="block text-sm font-medium text-text mb-1">
356 Start Date
357 </label>
358 <input
359 type="date"
360 value={formData.startDate}
361 onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
362 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
363 />
364 </div>
365
366 <div>
367 <label className="block text-sm font-medium text-text mb-1">
368 End Date
369 </label>
370 <input
371 type="date"
372 value={formData.endDate}
373 onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
374 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
375 />
376 </div>
377
378 <div>
379 <label className="block text-sm font-medium text-text mb-1">
380 Main Location
381 </label>
382 <input
383 type="text"
384 value={formData.mainLocation}
385 onChange={(e) => setFormData({ ...formData, mainLocation: e.target.value })}
386 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
387 placeholder="Store number"
388 />
389 </div>
390
391 <div>
392 <label className="block text-sm font-medium text-text mb-1">
393 Department ID
394 </label>
395 <input
396 type="text"
397 value={formData.departmentId}
398 onChange={(e) => setFormData({ ...formData, departmentId: e.target.value })}
399 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
400 />
401 </div>
402
403 <div>
404 <label className="block text-sm font-medium text-text mb-1">
405 Job Title
406 </label>
407 <input
408 type="text"
409 value={formData.jobTitle}
410 onChange={(e) => setFormData({ ...formData, jobTitle: e.target.value })}
411 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
412 />
413 </div>
414
415 <div>
416 <label className="block text-sm font-medium text-text mb-1">
417 Job Code
418 </label>
419 <input
420 type="text"
421 value={formData.jobCode}
422 onChange={(e) => setFormData({ ...formData, jobCode: e.target.value })}
423 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
424 />
425 </div>
426
427 <div>
428 <label className="block text-sm font-medium text-text mb-1">
429 NZTA UID
430 </label>
431 <input
432 type="text"
433 value={formData.nztaUid}
434 onChange={(e) => setFormData({ ...formData, nztaUid: e.target.value })}
435 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[#00543b] focus:border-transparent"
436 />
437 </div>
438 </div>
439 </div>
440
441 <div className="bg-surface-2 px-6 py-4 rounded-b-lg flex justify-end gap-3">
442 <button
443 onClick={() => setShowModal(false)}
444 className="px-6 py-2 border border-border rounded-lg hover:bg-surface-2 transition-colors"
445 >
446 Cancel
447 </button>
448 <button
449 onClick={handleSave}
450 className="bg-[#00543b] text-white px-6 py-2 rounded-lg hover:bg-[#003d2b] transition-colors"
451 >
452 {editingEmployee ? "Save Changes" : "Create Employee"}
453 </button>
454 </div>
455 </div>
456 </div>
457 )}
458 </div>
459 );
460}