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 StaffMember {
8 f100: number; // ID
9 f101: string; // Name
10 f102: number; // Active (1/0)
11 f107?: string; // Barcode
12 f121?: string; // Formal Name
13 f122?: string; // Main Location
14 f123?: string; // Printed Name
15 f124?: string; // First Name
16 f125?: string; // Last Name
17 f126?: string; // Nick Name
18 f127?: string; // Start Date
19 f128?: string; // Leave Date
20 f130?: string; // Private Email
21 f131?: string; // Work Email
22 f133?: string; // Online Login
23 f134?: number; // Is Customer Rep
24 f135?: number; // Is Account Rep
25 f136?: string; // Remote Profile
26 f137?: string; // Auto Login From
27 f143?: number; // Login Where bitmask
28 LastOk?: string; // Last seen datetime
29}
30
31interface SecurityRole {
32 id: number;
33 name: string;
34 active: boolean;
35}
36
37interface StaffFormData {
38 id?: number;
39 name: string;
40 active: boolean;
41 password: string;
42 barcode: string;
43 formalName: string;
44 mainLocation: string;
45 printedName: string;
46 firstName: string;
47 lastName: string;
48 nickName: string;
49 startDate: string;
50 leaveDate: string;
51 privateEmail: string;
52 workEmail: string;
53 onlineLogin: string;
54 onlinePassword: string;
55 isCustomerRep: boolean;
56 isAccountRep: boolean;
57 remoteProfile: number;
58 autoLoginFrom: string;
59 loginWhere: number;
60}
61
62interface Employee {
63 f100: number; // ID
64 f101: string; // Short Name
65 f102: string; // Full Name
66 f103?: string; // POS Login ID
67 f104?: string; // Start Date
68 f105?: string; // End Date
69 f106?: string; // Main Location
70 f107?: string; // Job Title
71 f108?: string; // Job Code
72 f109?: string; // Department ID
73 f110?: string; // External ID
74 f114?: string; // NZTA UID
75}
76
77export default function StaffManagementPage() {
78 const [activeTab, setActiveTab] = useState<'staff' | 'employees'>('staff');
79 const [staff, setStaff] = useState<StaffMember[]>([]);
80 const [filteredStaff, setFilteredStaff] = useState<StaffMember[]>([]);
81 const [securityRoles, setSecurityRoles] = useState<SecurityRole[]>([]);
82 const [selectedRoles, setSelectedRoles] = useState<number[]>([]);
83 const [loading, setLoading] = useState(true);
84 const [searchTerm, setSearchTerm] = useState("");
85 const [showActive, setShowActive] = useState(true);
86 const [showInactive, setShowInactive] = useState(true);
87 const [showRole, setShowRole] = useState(false);
88 const [showModal, setShowModal] = useState(false);
89 const [editingStaff, setEditingStaff] = useState<StaffMember | null>(null);
90
91 // Employees state
92 const [employees, setEmployees] = useState<Employee[]>([]);
93 const [filteredEmployees, setFilteredEmployees] = useState<Employee[]>([]);
94 const [employeeSearchTerm, setEmployeeSearchTerm] = useState("");
95 const [loadingEmployees, setLoadingEmployees] = useState(false);
96 const [showEmployeeModal, setShowEmployeeModal] = useState(false);
97 const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
98 const [formData, setFormData] = useState<StaffFormData>({
99 name: "",
100 active: true,
101 password: "",
102 barcode: "",
103 formalName: "",
104 mainLocation: "",
105 printedName: "",
106 firstName: "",
107 lastName: "",
108 nickName: "",
109 startDate: "",
110 leaveDate: "",
111 privateEmail: "",
112 workEmail: "",
113 onlineLogin: "",
114 onlinePassword: "",
115 isCustomerRep: false,
116 isAccountRep: false,
117 remoteProfile: 0,
118 autoLoginFrom: "",
119 loginWhere: 1, // POS by default
120 });
121
122 useEffect(() => {
123 loadStaff();
124 }, []);
125
126 useEffect(() => {
127 if (activeTab === 'employees') {
128 loadEmployees();
129 }
130 }, [activeTab]);
131
132 useEffect(() => {
133 applyEmployeeFilters();
134 }, [employees, employeeSearchTerm]);
135
136 useEffect(() => {
137 applyFilters();
138 }, [staff, searchTerm, showActive, showInactive, showRole]);
139
140 const loadStaff = async () => {
141 try {
142 setLoading(true);
143 const [staffResult, usageResult] = await Promise.all([
144 apiClient.getStaff(),
145 apiClient.getStaffUsage()
146 ]);
147
148 if (staffResult.success && staffResult.data?.DATS) {
149 const staffList = staffResult.data.DATS;
150
151 // Extract security roles (negative IDs)
152 const roles = staffList
153 .filter((s: StaffMember) => s.f100 < 0 && s.f102 === 1)
154 .map((s: StaffMember) => ({
155 id: s.f100,
156 name: s.f101,
157 active: s.f102 === 1,
158 }));
159 setSecurityRoles(roles);
160
161 // Merge usage data
162 if (usageResult.success && usageResult.data?.DATS) {
163 const usageMap = new Map(
164 usageResult.data.DATS.map((u: any) => [u.f110, u.f112])
165 );
166
167 staffList.forEach((s: StaffMember) => {
168 if (usageMap.has(s.f100)) {
169 s.LastOk = usageMap.get(s.f100) as string | undefined;
170 }
171 });
172 }
173
174 setStaff(staffList);
175 }
176 } catch (error) {
177 console.error("Error loading staff:", error);
178 } finally {
179 setLoading(false);
180 }
181 };
182
183 const applyFilters = () => {
184 let filtered = staff.filter((member) => {
185 const isActive = member.f102 === 1;
186 const isRole = member.f100 < 0;
187
188 // Filter by active/inactive/role
189 if (isRole && !showRole) return false;
190 if (!isRole && isActive && !showActive) return false;
191 if (!isRole && !isActive && !showInactive) return false;
192
193 // Filter by search term
194 if (searchTerm) {
195 const search = searchTerm.toLowerCase();
196 return (
197 member.f101?.toLowerCase().includes(search) ||
198 member.f107?.toLowerCase().includes(search) ||
199 member.f100?.toString().includes(search)
200 );
201 }
202
203 return true;
204 });
205
206 setFilteredStaff(filtered);
207 };
208
209 const openNewStaffModal = () => {
210 setEditingStaff(null);
211 setSelectedRoles([]);
212 setFormData({
213 name: "",
214 active: true,
215 password: "",
216 barcode: "",
217 formalName: "",
218 mainLocation: "",
219 printedName: "",
220 firstName: "",
221 lastName: "",
222 nickName: "",
223 startDate: "",
224 leaveDate: "",
225 privateEmail: "",
226 workEmail: "",
227 onlineLogin: "",
228 onlinePassword: "",
229 isCustomerRep: false,
230 isAccountRep: false,
231 remoteProfile: 0,
232 autoLoginFrom: "",
233 loginWhere: 1,
234 });
235 setShowModal(true);
236 };
237
238 const openEditModal = (member: StaffMember) => {
239 setEditingStaff(member);
240 setFormData({
241 id: member.f100,
242 name: member.f101 || "",
243 active: member.f102 === 1,
244 password: "",
245 barcode: member.f107 || "",
246 formalName: member.f121 || "",
247 mainLocation: member.f122 || "",
248 printedName: member.f123 || "",
249 firstName: member.f124 || "",
250 lastName: member.f125 || "",
251 nickName: member.f126 || "",
252 startDate: member.f127 || "",
253 leaveDate: member.f128 || "",
254 privateEmail: member.f130 || "",
255 workEmail: member.f131 || "",
256 onlineLogin: member.f133 || "",
257 onlinePassword: "",
258 isCustomerRep: member.f134 === 1,
259 isAccountRep: member.f135 === 1,
260 remoteProfile: parseInt(member.f136 || "0"),
261 autoLoginFrom: member.f137 || "",
262 loginWhere: member.f143 || 1,
263 });
264 setShowModal(true);
265 };
266
267 const handleSave = async () => {
268 try {
269 const saveData: any = { ...formData };
270
271 // Add selected roles for new staff
272 if (!editingStaff && selectedRoles.length > 0) {
273 saveData.roles = selectedRoles;
274 }
275
276 const result = await apiClient.saveStaff(saveData);
277
278 if (result.success) {
279 setShowModal(false);
280 loadStaff();
281 } else {
282 alert("Failed to save staff: " + (result.error || "Unknown error"));
283 }
284 } catch (error) {
285 console.error("Error saving staff:", error);
286 alert("Failed to save staff member");
287 }
288 };
289
290 const formatDateTime = (dateStr: string) => {
291 if (!dateStr) return "—";
292 const date = new Date(dateStr);
293 return date.toLocaleString("en-GB", {
294 day: "2-digit",
295 month: "short",
296 year: "numeric",
297 hour: "2-digit",
298 minute: "2-digit",
299 });
300 };
301
302 // Login log functions
303 const loadEmployees = async () => {
304 try {
305 setLoadingEmployees(true);
306 const result = await apiClient.getEmployees();
307
308 if (result.success && result.data?.DATS) {
309 setEmployees(result.data.DATS);
310 setFilteredEmployees(result.data.DATS);
311 }
312 } catch (error) {
313 console.error("Error loading employees:", error);
314 } finally {
315 setLoadingEmployees(false);
316 }
317 };
318
319 const applyEmployeeFilters = () => {
320 if (!employeeSearchTerm) {
321 setFilteredEmployees(employees);
322 return;
323 }
324
325 const search = employeeSearchTerm.toLowerCase();
326 const filtered = employees.filter((emp) => {
327 return (
328 emp.f100?.toString().includes(search) ||
329 emp.f101?.toLowerCase().includes(search) ||
330 emp.f102?.toLowerCase().includes(search) ||
331 emp.f107?.toLowerCase().includes(search) ||
332 emp.f110?.toLowerCase().includes(search)
333 );
334 });
335
336 setFilteredEmployees(filtered);
337 };
338
339 const openNewEmployeeModal = () => {
340 setEditingEmployee({
341 f100: 0,
342 f101: "",
343 f102: "",
344 f103: "",
345 f104: "",
346 f105: "",
347 f106: "",
348 f107: "",
349 f108: "",
350 f109: "",
351 f110: "",
352 f114: "",
353 });
354 setShowEmployeeModal(true);
355 };
356
357 const openEditEmployeeModal = (employee: Employee) => {
358 setEditingEmployee(employee);
359 setShowEmployeeModal(true);
360 };
361
362 const handleSaveEmployee = async () => {
363 try {
364 const saveData: any = {
365 shortName: editingEmployee?.f101 || "",
366 fullName: editingEmployee?.f102 || "",
367 posLoginId: editingEmployee?.f103,
368 startDate: editingEmployee?.f104,
369 endDate: editingEmployee?.f105,
370 mainLocation: editingEmployee?.f106,
371 jobTitle: editingEmployee?.f107,
372 jobCode: editingEmployee?.f108,
373 departmentId: editingEmployee?.f109,
374 externalId: editingEmployee?.f110,
375 nztaUid: editingEmployee?.f114,
376 };
377
378 if (editingEmployee?.f100) {
379 saveData.id = editingEmployee.f100;
380 }
381
382 const result = await apiClient.saveEmployee(saveData);
383
384 if (result.success) {
385 setShowEmployeeModal(false);
386 setEditingEmployee(null);
387 loadEmployees();
388 } else {
389 alert("Failed to save employee: " + (result.error || "Unknown error"));
390 }
391 } catch (error) {
392 console.error("Error saving employee:", error);
393 alert("Failed to save employee");
394 }
395 };
396
397 if (loading) {
398 return (
399 <div className="p-6 bg-bg min-h-screen">
400 <div className="animate-pulse space-y-4">
401 <div className="h-8 bg-surface rounded w-1/4"></div>
402 <div className="h-64 bg-surface rounded"></div>
403 </div>
404 </div>
405 );
406 }
407
408 return (
409 <div className="p-6 bg-bg min-h-screen">
410 {/* Header */}
411 <div className="mb-6 flex items-center gap-3">
412 <Icon name="badge" size={36} className="text-brand" />
413 <div>
414 <h1 className="text-3xl font-bold text-text">
415 Workforce Management
416 </h1>
417 <p className="text-muted">
418 Manage POS staff, employees, security roles, and login history
419 </p>
420 </div>
421 </div>
422
423 {/* Info Cards */}
424 <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
425 <div className="bg-brand/10 border-l-4 border-brand p-6 rounded-lg">
426 <div className="flex items-start gap-4">
427 <Icon name="person" size={40} className="text-brand flex-shrink-0" />
428 <div>
429 <h3 className="text-lg font-bold text-text mb-2 flex items-center gap-2">
430 <Icon name="person" size={20} />Staff (POS Operators)
431 </h3>
432 <p className="text-sm text-text mb-2">
433 <strong>POS system users</strong> with login credentials for cash registers and terminals.
434 </p>
435 <ul className="text-xs text-muted space-y-1">
436 <li>• Have POS login access and passwords</li>
437 <li>• Assigned to security roles for permissions</li>
438 <li>• Track sales, transactions, and till operations</li>
439 <li>• Can log in to selling interface</li>
440 </ul>
441 </div>
442 </div>
443 </div>
444
445 <div className="bg-success/10 border-l-4 border-success p-6 rounded-lg">
446 <div className="flex items-start gap-4">
447 <Icon name="groups" size={40} className="text-success flex-shrink-0" />
448 <div>
449 <h3 className="text-lg font-bold text-text mb-2 flex items-center gap-2">
450 <Icon name="groups" size={20} />Employees (HR Records)
451 </h3>
452 <p className="text-sm text-text mb-2">
453 <strong>Workforce records</strong> for commission tracking, payroll, and HR management.
454 </p>
455 <ul className="text-xs text-muted space-y-1">
456 <li>• NO POS login access</li>
457 <li>• Track commissions and payroll data</li>
458 <li>• Job titles, departments, hire dates</li>
459 <li>• May be linked to external payroll systems</li>
460 </ul>
461 </div>
462 </div>
463 </div>
464 </div>
465
466 {/* Tab Navigation */}
467 <div className="bg-surface rounded-lg shadow mb-6 p-2">
468 <div className="grid grid-cols-2 gap-2">
469 <button
470 onClick={() => setActiveTab('staff')}
471 className={`px-4 py-4 font-semibold text-base rounded-lg transition-all ${
472 activeTab === 'staff'
473 ? 'bg-brand text-white shadow-lg transform scale-105'
474 : 'bg-surface-2 text-muted hover:bg-surface'
475 }`}
476 >
477 <div className="flex flex-col items-center justify-center gap-1">
478 <Icon name="person" size={24} />
479 <span>Staff (POS)</span>
480 </div>
481 </button>
482 <button
483 onClick={() => setActiveTab('employees')}
484 className={`px-4 py-4 font-semibold text-base rounded-lg transition-all ${
485 activeTab === 'employees'
486 ? 'bg-success text-white shadow-lg transform scale-105'
487 : 'bg-surface-2 text-muted hover:bg-surface'
488 }`}
489 >
490 <div className="flex flex-col items-center justify-center gap-1">
491 <Icon name="groups" size={24} />
492 <span>Employees (HR)</span>
493 </div>
494 </button>
495 </div>
496 </div>
497
498 {/* Staff List View */}
499 {activeTab === 'staff' && (
500 <>
501 {/* Action Bar */}
502 <div className="bg-surface rounded-lg shadow p-4 mb-6">
503 <div className="flex flex-wrap items-center gap-4">
504 <button
505 onClick={openNewStaffModal}
506 className="bg-brand text-white px-6 py-2 rounded-lg hover:bg-brand/90 transition-colors flex items-center gap-2"
507 >
508 <Icon name="add" size={20} />
509 New Staff Member
510 </button>
511
512 <div className="flex-1 min-w-[200px] relative">
513 <Icon name="search" size={20} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted" />
514 <input
515 type="text"
516 placeholder="Search staff..."
517 value={searchTerm}
518 onChange={(e) => setSearchTerm(e.target.value)}
519 className="w-full pl-10 pr-4 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
520 />
521 </div>
522
523 <div className="flex items-center gap-4">
524 <label className="flex items-center gap-2 cursor-pointer">
525 <input
526 type="checkbox"
527 checked={showActive}
528 onChange={(e) => setShowActive(e.target.checked)}
529 className="w-4 h-4 text-brand rounded"
530 />
531 <span className="text-sm text-text">Active</span>
532 </label>
533 <label className="flex items-center gap-2 cursor-pointer">
534 <input
535 type="checkbox"
536 checked={showInactive}
537 onChange={(e) => setShowInactive(e.target.checked)}
538 className="w-4 h-4 text-brand rounded"
539 />
540 <span className="text-sm text-text">Inactive</span>
541 </label>
542 <label className="flex items-center gap-2 cursor-pointer">
543 <input
544 type="checkbox"
545 checked={showRole}
546 onChange={(e) => setShowRole(e.target.checked)}
547 className="w-4 h-4 text-brand rounded"
548 />
549 <span className="text-sm text-text">Roles Only</span>
550 </label>
551 </div>
552 </div>
553
554 <div className="mt-2 text-sm text-muted">
555 Displaying {filteredStaff.length} of {staff.length} staff members
556 </div>
557 </div>
558
559 {/* Staff Table */}
560 <div className="bg-surface rounded-lg shadow overflow-hidden">
561 <div className="overflow-x-auto">
562 <table className="w-full">
563 <thead className="bg-[var(--brand)] text-surface">
564 <tr>
565 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
566 ID
567 </th>
568 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
569 Name
570 </th>
571 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
572 Status
573 </th>
574 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
575 Barcode
576 </th>
577 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
578 Last Seen
579 </th>
580 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
581 Actions
582 </th>
583 </tr>
584 </thead>
585 <tbody className="bg-surface divide-y divide-border">
586 {filteredStaff.map((member) => (
587 <tr key={member.f100} className="hover:bg-surface-2">
588 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
589 {member.f100}
590 </td>
591 <td className="px-6 py-4 whitespace-nowrap">
592 <div className="text-sm font-medium text-text">
593 {member.f101}
594 </div>
595 {member.f121 && (
596 <div className="text-sm text-muted">{member.f121}</div>
597 )}
598 </td>
599 <td className="px-6 py-4 whitespace-nowrap">
600 <span
601 className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
602 member.f102 === 1
603 ? "bg-success/20 text-success"
604 : "bg-danger/20 text-danger"
605 }`}
606 >
607 {member.f102 === 1 ? "Active" : "Inactive"}
608 </span>
609 </td>
610 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
611 {member.f107 || "—"}
612 </td>
613 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
614 {formatDateTime(member.LastOk || "")}
615 </td>
616 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
617 <button
618 onClick={() => openEditModal(member)}
619 className="text-[#00543b] hover:text-[#003d2b] mr-4"
620 >
621 Edit
622 </button>
623 </td>
624 </tr>
625 ))}
626 </tbody>
627 </table>
628 </div>
629 </div>
630 </>
631 )}
632
633 {/* Employees View */}
634 {activeTab === 'employees' && (
635 <>
636 <div className="bg-surface rounded-lg shadow p-4 mb-6">
637 <div className="flex flex-wrap items-center gap-4">
638 <button
639 onClick={openNewEmployeeModal}
640 className="bg-success text-white px-6 py-2 rounded-lg hover:bg-success/90 transition-colors flex items-center gap-2"
641 >
642 <Icon name="add" size={20} />
643 New Employee
644 </button>
645
646 <div className="flex-1 min-w-[200px] relative">
647 <Icon name="search" size={20} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted" />
648 <input
649 type="text"
650 placeholder="Search employees..."
651 value={employeeSearchTerm}
652 onChange={(e) => setEmployeeSearchTerm(e.target.value)}
653 className="w-full pl-10 pr-4 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-success focus:border-transparent text-text"
654 />
655 </div>
656 </div>
657 </div>
658
659 <div className="bg-surface rounded-lg shadow overflow-hidden">
660 {loadingEmployees ? (
661 <div className="p-8 text-center">
662 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-success mx-auto"></div>
663 <p className="mt-4 text-muted">Loading employees...</p>
664 </div>
665 ) : filteredEmployees.length === 0 ? (
666 <div className="p-8 text-center text-muted">
667 <Icon name="groups" size={64} className="mx-auto text-muted/50 mb-4" />
668 <p className="text-lg font-semibold mb-2 text-text">No employees found</p>
669 <p className="text-sm">
670 {employeeSearchTerm ? `No employees match "${employeeSearchTerm}"` : "No employee records available"}
671 </p>
672 </div>
673 ) : (
674 <div className="overflow-x-auto">
675 <table className="w-full">
676 <thead className="bg-success/10">
677 <tr>
678 <th className="px-6 py-3 text-left text-xs font-medium text-text uppercase tracking-wider">ID</th>
679 <th className="px-6 py-3 text-left text-xs font-medium text-text uppercase tracking-wider">Short Name</th>
680 <th className="px-6 py-3 text-left text-xs font-medium text-text uppercase tracking-wider">Full Name</th>
681 <th className="px-6 py-3 text-left text-xs font-medium text-text uppercase tracking-wider">Job Title</th>
682 <th className="px-6 py-3 text-left text-xs font-medium text-text uppercase tracking-wider">Department</th>
683 <th className="px-6 py-3 text-left text-xs font-medium text-text uppercase tracking-wider">Start Date</th>
684 <th className="px-6 py-3 text-left text-xs font-medium text-text uppercase tracking-wider">External ID</th>
685 <th className="px-6 py-3 text-left text-xs font-medium text-text uppercase tracking-wider">Actions</th>
686 </tr>
687 </thead>
688 <tbody className="bg-surface divide-y divide-border">
689 {filteredEmployees.map((emp) => (
690 <tr key={emp.f100} className="hover:bg-surface-2">
691 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text">
692 {emp.f100}
693 </td>
694 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
695 {emp.f101 || "—"}
696 </td>
697 <td className="px-6 py-4 text-sm text-text">
698 {emp.f102 || "—"}
699 </td>
700 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
701 {emp.f107 || "—"}
702 </td>
703 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
704 {emp.f109 || "—"}
705 </td>
706 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
707 {emp.f104 ? new Date(emp.f104).toLocaleDateString() : "—"}
708 </td>
709 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
710 {emp.f110 || "—"}
711 </td>
712 <td className="px-6 py-4 whitespace-nowrap text-sm">
713 <button
714 onClick={() => openEditEmployeeModal(emp)}
715 className="text-success hover:text-success/80 font-medium flex items-center gap-1"
716 >
717 <Icon name="edit" size={16} />
718 Edit
719 </button>
720 </td>
721 </tr>
722 ))}
723 </tbody>
724 </table>
725 </div>
726 )}
727 </div>
728
729 <div className="mt-6 bg-success/10 border-l-4 border-success p-4 rounded">
730 <div className="flex items-start gap-3">
731 <Icon name="info" size={20} className="text-success flex-shrink-0 mt-0.5" />
732 <div className="text-sm text-text">
733 <p className="font-semibold mb-1">About Employees</p>
734 <p>Employees are tracked for commission, payroll, and HR purposes. They <strong>do not</strong> have POS login access. If someone needs to operate a cash register, add them as <strong>Staff</strong> instead.</p>
735 </div>
736 </div>
737 </div>
738 </>
739 )}
740
741 {/* Edit/Create Modal */}
742 {showModal && (
743 <div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4 overflow-y-auto">
744 <div className="bg-surface backdrop-blur-md rounded-lg shadow-xl max-w-4xl w-full my-8">
745 <div className="bg-brand backdrop-blur-sm text-white px-6 py-4 rounded-t-lg flex items-center justify-between">
746 <h2 className="text-2xl font-bold text-white flex items-center gap-2">
747 <Icon name="person" size={28} />
748 {editingStaff ? "Edit Staff Member" : "New Staff Member"}
749 </h2>
750 <button
751 onClick={() => setShowModal(false)}
752 className="text-white hover:text-muted/70"
753 >
754 <Icon name="close" size={24} />
755 </button>
756 </div>
757
758 <div className="p-6 max-h-[70vh] overflow-y-auto bg-surface backdrop-blur-md rounded-b-lg">
759 <div className="grid grid-cols-2 gap-6">
760 <div>
761 <label className="block text-sm font-medium text-text mb-1">
762 Name *
763 </label>
764 <input
765 type="text"
766 value={formData.name}
767 onChange={(e) => setFormData({ ...formData, name: e.target.value })}
768 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
769 required
770 />
771 </div>
772
773 <div>
774 <label className="block text-sm font-medium text-text mb-1">
775 Password
776 </label>
777 <input
778 type="password"
779 value={formData.password}
780 onChange={(e) => setFormData({ ...formData, password: e.target.value })}
781 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
782 placeholder={editingStaff ? "Leave blank to keep current" : ""}
783 />
784 </div>
785
786 <div>
787 <label className="flex items-center gap-2 cursor-pointer">
788 <input
789 type="checkbox"
790 checked={formData.active}
791 onChange={(e) => setFormData({ ...formData, active: e.target.checked })}
792 className="w-4 h-4 text-brand rounded"
793 disabled={!editingStaff}
794 />
795 <span className="text-sm font-medium text-text">Active</span>
796 </label>
797 {!editingStaff && (
798 <p className="text-xs text-muted mt-1">
799 All staff are created active. Edit after creation to disable.
800 </p>
801 )}
802 </div>
803
804 {/* Security Roles - Only show for new staff */}
805 {!editingStaff && securityRoles.length > 0 && (
806 <div className="col-span-2">
807 <label className="block text-sm font-medium text-text mb-2">
808 Assign Security Roles
809 </label>
810 <div className="border border-border rounded-lg p-4 bg-bg max-h-40 overflow-y-auto">
811 <div className="space-y-2">
812 {securityRoles.map((role) => (
813 <label key={role.id} className="flex items-center gap-2 cursor-pointer">
814 <input
815 type="checkbox"
816 checked={selectedRoles.includes(role.id)}
817 onChange={(e) => {
818 if (e.target.checked) {
819 setSelectedRoles([...selectedRoles, role.id]);
820 } else {
821 setSelectedRoles(selectedRoles.filter(id => id !== role.id));
822 }
823 }}
824 className="w-4 h-4 text-brand rounded"
825 />
826 <span className="text-sm text-text">{role.name}</span>
827 </label>
828 ))}
829 </div>
830 </div>
831 </div>
832 )}
833
834 <div className="col-span-2">
835 <label className="block text-sm font-medium text-text mb-2">
836 Login Where
837 </label>
838 <div className="flex flex-wrap gap-4">
839 <label className="flex items-center gap-2 cursor-pointer">
840 <input
841 type="checkbox"
842 checked={(formData.loginWhere & 1) === 0}
843 onChange={(e) => {
844 const bit = 1;
845 setFormData({
846 ...formData,
847 loginWhere: e.target.checked
848 ? formData.loginWhere & ~bit
849 : formData.loginWhere | bit,
850 });
851 }}
852 className="w-4 h-4 text-brand rounded"
853 />
854 <span className="text-sm text-text">POS</span>
855 </label>
856 <label className="flex items-center gap-2 cursor-pointer">
857 <input
858 type="checkbox"
859 checked={(formData.loginWhere & 2) !== 0}
860 onChange={(e) => {
861 const bit = 2;
862 setFormData({
863 ...formData,
864 loginWhere: e.target.checked
865 ? formData.loginWhere | bit
866 : formData.loginWhere & ~bit,
867 });
868 }}
869 className="w-4 h-4 text-brand rounded"
870 />
871 <span className="text-sm text-text">Store Server</span>
872 </label>
873 <label className="flex items-center gap-2 cursor-pointer">
874 <input
875 type="checkbox"
876 checked={(formData.loginWhere & 4) !== 0}
877 onChange={(e) => {
878 const bit = 4;
879 setFormData({
880 ...formData,
881 loginWhere: e.target.checked
882 ? formData.loginWhere | bit
883 : formData.loginWhere & ~bit,
884 });
885 }}
886 className="w-4 h-4 text-brand rounded"
887 />
888 <span className="text-sm text-text">Head Office</span>
889 </label>
890 </div>
891 <p className="text-xs text-muted mt-1">
892 Tick all options for full access user (only effective if security enabled)
893 </p>
894 </div>
895
896 <div>
897 <label className="block text-sm font-medium text-text mb-1">
898 Login Barcode
899 </label>
900 <input
901 type="text"
902 value={formData.barcode}
903 onChange={(e) => setFormData({ ...formData, barcode: e.target.value })}
904 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
905 />
906 </div>
907
908 <div>
909 <label className="block text-sm font-medium text-text mb-1">
910 Formal Name
911 </label>
912 <input
913 type="text"
914 value={formData.formalName}
915 onChange={(e) => setFormData({ ...formData, formalName: e.target.value })}
916 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
917 />
918 </div>
919
920 <div>
921 <label className="block text-sm font-medium text-text mb-1">
922 Printed Name
923 </label>
924 <input
925 type="text"
926 value={formData.printedName}
927 onChange={(e) => setFormData({ ...formData, printedName: e.target.value })}
928 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
929 />
930 </div>
931
932 <div>
933 <label className="block text-sm font-medium text-text mb-1">
934 First Name
935 </label>
936 <input
937 type="text"
938 value={formData.firstName}
939 onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
940 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
941 />
942 </div>
943
944 <div>
945 <label className="block text-sm font-medium text-text mb-1">
946 Last Name
947 </label>
948 <input
949 type="text"
950 value={formData.lastName}
951 onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
952 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
953 />
954 </div>
955
956 <div>
957 <label className="block text-sm font-medium text-text mb-1">
958 Nick Name
959 </label>
960 <input
961 type="text"
962 value={formData.nickName}
963 onChange={(e) => setFormData({ ...formData, nickName: e.target.value })}
964 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
965 />
966 </div>
967
968 <div>
969 <label className="block text-sm font-medium text-text mb-1">
970 Main Location
971 </label>
972 <input
973 type="text"
974 value={formData.mainLocation}
975 onChange={(e) => setFormData({ ...formData, mainLocation: e.target.value })}
976 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
977 placeholder="Store number"
978 />
979 <p className="text-xs text-muted mt-1">
980 Location number where staff typically works (not enforced)
981 </p>
982 </div>
983
984 <div>
985 <label className="block text-sm font-medium text-text mb-1">
986 Remote Profile
987 </label>
988 <select
989 value={formData.remoteProfile}
990 onChange={(e) => setFormData({ ...formData, remoteProfile: parseInt(e.target.value) })}
991 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
992 >
993 <option value={0}>None</option>
994 <option value={1}>Profile 1</option>
995 <option value={2}>Profile 2</option>
996 <option value={3}>Profile 3</option>
997 <option value={4}>Profile 4</option>
998 <option value={5}>Profile 5</option>
999 <option value={6}>Profile 6</option>
1000 <option value={7}>Profile 7</option>
1001 <option value={8}>Profile 8</option>
1002 </select>
1003 <p className="text-xs text-muted mt-1">
1004 Profile for remote access terminal usage
1005 </p>
1006 </div>
1007
1008 <div>
1009 <label className="block text-sm font-medium text-text mb-1">
1010 Auto Login From
1011 </label>
1012 <input
1013 type="text"
1014 value={formData.autoLoginFrom}
1015 onChange={(e) => setFormData({ ...formData, autoLoginFrom: e.target.value })}
1016 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
1017 placeholder="Windows login name"
1018 />
1019 <p className="text-xs text-muted mt-1">
1020 Auto login to POS if using this Windows login (must be enabled on lane)
1021 </p>
1022 </div>
1023
1024 <div>
1025 <label className="block text-sm font-medium text-text mb-1">
1026 Work Email
1027 </label>
1028 <input
1029 type="email"
1030 value={formData.workEmail}
1031 onChange={(e) => setFormData({ ...formData, workEmail: e.target.value })}
1032 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
1033 />
1034 </div>
1035
1036 <div>
1037 <label className="block text-sm font-medium text-text mb-1">
1038 Private Email
1039 </label>
1040 <input
1041 type="email"
1042 value={formData.privateEmail}
1043 onChange={(e) => setFormData({ ...formData, privateEmail: e.target.value })}
1044 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
1045 />
1046 </div>
1047
1048 <div>
1049 <label className="block text-sm font-medium text-text mb-1">
1050 Online Login Email
1051 </label>
1052 <input
1053 type="email"
1054 value={formData.onlineLogin}
1055 onChange={(e) => setFormData({ ...formData, onlineLogin: e.target.value })}
1056 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
1057 />
1058 </div>
1059
1060 {editingStaff && (
1061 <div>
1062 <label className="block text-sm font-medium text-text mb-1">
1063 Online Password
1064 </label>
1065 <input
1066 type="password"
1067 value={formData.onlinePassword}
1068 onChange={(e) => setFormData({ ...formData, onlinePassword: e.target.value })}
1069 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
1070 />
1071 </div>
1072 )}
1073
1074 <div>
1075 <label className="block text-sm font-medium text-text mb-1">
1076 Start Date
1077 </label>
1078 <input
1079 type="date"
1080 value={formData.startDate}
1081 onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
1082 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
1083 />
1084 </div>
1085
1086 <div>
1087 <label className="block text-sm font-medium text-text mb-1">
1088 Leave Date
1089 </label>
1090 <input
1091 type="date"
1092 value={formData.leaveDate}
1093 onChange={(e) => setFormData({ ...formData, leaveDate: e.target.value })}
1094 className="w-full px-3 py-2 bg-bg border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-text"
1095 />
1096 </div>
1097
1098 <div className="col-span-2">
1099 <label className="block text-sm font-medium text-text mb-2">
1100 Flags
1101 </label>
1102 <div className="flex gap-6">
1103 <label className="flex items-center gap-2 cursor-pointer">
1104 <input
1105 type="checkbox"
1106 checked={formData.isCustomerRep}
1107 onChange={(e) => setFormData({ ...formData, isCustomerRep: e.target.checked })}
1108 className="w-4 h-4 text-brand rounded"
1109 />
1110 <span className="text-sm text-text">Is Customer Rep</span>
1111 </label>
1112 <label className="flex items-center gap-2 cursor-pointer">
1113 <input
1114 type="checkbox"
1115 checked={formData.isAccountRep}
1116 onChange={(e) => setFormData({ ...formData, isAccountRep: e.target.checked })}
1117 className="w-4 h-4 text-brand rounded"
1118 />
1119 <span className="text-sm text-text">Is Sales/Account Rep</span>
1120 </label>
1121 </div>
1122 </div>
1123 </div>
1124 </div>
1125
1126 <div className="bg-surface-2 px-6 py-4 rounded-b-lg flex justify-end gap-3">
1127 <button
1128 onClick={() => setShowModal(false)}
1129 className="px-6 py-2 border border-border rounded-lg hover:bg-surface transition-colors text-text"
1130 >
1131 Cancel
1132 </button>
1133 <button
1134 onClick={handleSave}
1135 className="bg-brand text-white px-6 py-2 rounded-lg hover:bg-brand/90 transition-colors"
1136 >
1137 {editingStaff ? "Save Changes" : "Create Staff Member"}
1138 </button>
1139 </div>
1140 </div>
1141 </div>
1142 )}
1143
1144 {/* Employee Edit/Create Modal */}
1145 {showEmployeeModal && (
1146 <div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4 overflow-y-auto">
1147 <div className="bg-surface backdrop-blur-md rounded-lg shadow-xl max-w-3xl w-full my-8">
1148 <div className="bg-success backdrop-blur-sm text-white px-6 py-4 rounded-t-lg flex items-center justify-between">
1149 <h2 className="text-2xl font-bold text-white flex items-center gap-2">
1150 <Icon name="groups" size={28} />
1151 {editingEmployee ? "Edit Employee" : "New Employee"}
1152 </h2>
1153 <button
1154 onClick={() => {
1155 setShowEmployeeModal(false);
1156 setEditingEmployee(null);
1157 }}
1158 className="text-white hover:text-muted/70"
1159 >
1160 <Icon name="close" size={24} />
1161 </button>
1162 </div>
1163
1164 <div className="p-6 max-h-[70vh] overflow-y-auto bg-surface backdrop-blur-md">
1165 <div className="grid grid-cols-2 gap-6">
1166 <div>
1167 <label className="block text-sm font-medium text-text mb-1">
1168 Short Name <span className="text-danger">*</span>
1169 </label>
1170 <input
1171 type="text"
1172 value={editingEmployee?.f101 || ""}
1173 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f101: e.target.value })}
1174 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1175 placeholder="Short display name"
1176 />
1177 </div>
1178
1179 <div>
1180 <label className="block text-sm font-medium text-text mb-1">
1181 Full Name <span className="text-danger">*</span>
1182 </label>
1183 <input
1184 type="text"
1185 value={editingEmployee?.f102 || ""}
1186 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f102: e.target.value })}
1187 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1188 placeholder="Full legal name"
1189 />
1190 </div>
1191
1192 <div>
1193 <label className="block text-sm font-medium text-text mb-1">
1194 POS Login ID
1195 </label>
1196 <input
1197 type="text"
1198 value={editingEmployee?.f103 || ""}
1199 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f103: e.target.value })}
1200 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1201 placeholder="Optional POS login reference"
1202 />
1203 </div>
1204
1205 <div>
1206 <label className="block text-sm font-medium text-text mb-1">
1207 Job Title
1208 </label>
1209 <input
1210 type="text"
1211 value={editingEmployee?.f107 || ""}
1212 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f107: e.target.value })}
1213 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1214 placeholder="e.g. Sales Associate"
1215 />
1216 </div>
1217
1218 <div>
1219 <label className="block text-sm font-medium text-text mb-1">
1220 Job Code
1221 </label>
1222 <input
1223 type="text"
1224 value={editingEmployee?.f108 || ""}
1225 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f108: e.target.value })}
1226 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1227 placeholder="Internal job code"
1228 />
1229 </div>
1230
1231 <div>
1232 <label className="block text-sm font-medium text-text mb-1">
1233 Department ID
1234 </label>
1235 <input
1236 type="text"
1237 value={editingEmployee?.f109 || ""}
1238 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f109: e.target.value })}
1239 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1240 placeholder="Department identifier"
1241 />
1242 </div>
1243
1244 <div>
1245 <label className="block text-sm font-medium text-text mb-1">
1246 Main Location
1247 </label>
1248 <input
1249 type="text"
1250 value={editingEmployee?.f106 || ""}
1251 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f106: e.target.value })}
1252 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1253 placeholder="Primary work location"
1254 />
1255 </div>
1256
1257 <div>
1258 <label className="block text-sm font-medium text-text mb-1">
1259 External ID
1260 </label>
1261 <input
1262 type="text"
1263 value={editingEmployee?.f110 || ""}
1264 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f110: e.target.value })}
1265 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1266 placeholder="External system ID"
1267 />
1268 </div>
1269
1270 <div>
1271 <label className="block text-sm font-medium text-text mb-1">
1272 Start Date
1273 </label>
1274 <input
1275 type="date"
1276 value={editingEmployee?.f104 || ""}
1277 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f104: e.target.value })}
1278 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1279 />
1280 </div>
1281
1282 <div>
1283 <label className="block text-sm font-medium text-text mb-1">
1284 End Date
1285 </label>
1286 <input
1287 type="date"
1288 value={editingEmployee?.f105 || ""}
1289 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f105: e.target.value })}
1290 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1291 />
1292 </div>
1293
1294 <div>
1295 <label className="block text-sm font-medium text-text mb-1">
1296 NZTA UID
1297 </label>
1298 <input
1299 type="text"
1300 value={editingEmployee?.f114 || ""}
1301 onChange={(e) => setEditingEmployee({ ...editingEmployee!, f114: e.target.value })}
1302 className="w-full px-3 py-2 bg-bg border border-border rounded focus:ring-2 focus:ring-success focus:border-transparent text-text"
1303 placeholder="NZ Transport Agency ID"
1304 />
1305 </div>
1306 </div>
1307 </div>
1308
1309 <div className="bg-surface-2 px-6 py-4 rounded-b-lg flex justify-end gap-3">
1310 <button
1311 onClick={() => {
1312 setShowEmployeeModal(false);
1313 setEditingEmployee(null);
1314 }}
1315 className="px-6 py-2 border border-border rounded-lg hover:bg-surface transition-colors text-text"
1316 >
1317 Cancel
1318 </button>
1319 <button
1320 onClick={handleSaveEmployee}
1321 className="bg-success text-white px-6 py-2 rounded-lg hover:bg-success/90 transition-colors"
1322 disabled={!editingEmployee?.f101 || !editingEmployee?.f102}
1323 >
1324 {editingEmployee?.f100 ? "Save Changes" : "Create Employee"}
1325 </button>
1326 </div>
1327 </div>
1328 </div>
1329 )}
1330 </div>
1331 );
1332}