3import { useState, useEffect } from 'react';
4import Link from 'next/link';
5import { Icon } from '@/contexts/IconContext';
10 f102: string; // Location/Store
11 f104: string; // Store Name
12 f107: string; // Last Seen
13 f110: number; // Authorized
14 f114: string; // IP Address
15 f124: string; // Comments
16 f125: string; // Asset Number
17 f126: number; // Life Phase
18 f128: number; // Profile
19 f129: string; // Active UI
20 f130: string; // Set UI
21 f131: number; // Canary Level
22 f132: string; // Auto Update
23 uistr?: string; // UI display string
34export default function LanesPage() {
35 const [lanes, setLanes] = useState<Lane[]>([]);
36 const [filteredLanes, setFilteredLanes] = useState<Lane[]>([]);
37 const [profiles, setProfiles] = useState<Profile[]>([]);
38 const [loading, setLoading] = useState(true);
39 const [searchTerm, setSearchTerm] = useState('');
40 const [authFilter, setAuthFilter] = useState('0'); // 0=All, 1=Only Auth, 2=Only Non-Auth
41 const [phaseFilter, setPhaseFilter] = useState('-1'); // -3=All, -1=All Active, -2=All Non-Active, 0-3=specific, 64-66=retired/test/deployed
43 const [showEditModal, setShowEditModal] = useState(false);
44 const [showProfileModal, setShowProfileModal] = useState(false);
45 const [showScanModal, setShowScanModal] = useState(false);
46 const [editingLane, setEditingLane] = useState<Lane | null>(null);
48 const [laneForm, setLaneForm] = useState({
70 }, [lanes, searchTerm, authFilter, phaseFilter]);
72 const loadData = async () => {
75 // In production: await apiClient.getLanes()
76 // For now, using placeholder data
77 const mockLanes: Lane[] = [
83 f107: '2025-12-24T10:30:00',
85 f114: '192.168.1.100',
90 f129: 'Touch Screen UI',
91 f130: 'Touch Screen UI',
94 uistr: 'Touch Screen UI',
96 Time: '24-Dec-2025 10:30'
103 f107: '2025-12-24T09:15:00',
105 f114: '192.168.1.101',
106 f124: 'Back counter',
110 f129: 'Touch Screen UI',
111 f130: 'Touch Screen UI',
114 uistr: 'Touch Screen UI',
116 Time: '24-Dec-2025 09:15'
123 f107: '2025-12-24T08:00:00',
125 f114: '192.168.1.150',
126 f124: 'Office system',
130 f129: 'Default POS Interface',
131 f130: 'Default POS Interface',
134 uistr: 'Default POS Interface',
136 Time: '24-Dec-2025 08:00'
143 const mockProfiles: Profile[] = [
144 { id: 1, name: 'Selling Counters', used: 2 },
145 { id: 2, name: 'Back Office', used: 1 },
146 { id: 3, name: 'Profile 3', used: 0 },
147 { id: 4, name: 'Profile 4', used: 0 },
148 { id: 5, name: 'Profile 5', used: 0 },
149 { id: 6, name: 'Profile 6', used: 0 },
150 { id: 7, name: 'Profile 7', used: 0 },
151 { id: 8, name: 'Profile 8', used: 0 }
153 setProfiles(mockProfiles);
155 console.error('Error loading lanes:', error);
156 alert('Failed to load lanes data');
162 const applyFilters = () => {
163 let filtered = [...lanes];
166 if (authFilter === '1') {
167 filtered = filtered.filter(l => l.f110 === 1);
168 } else if (authFilter === '2') {
169 filtered = filtered.filter(l => l.f110 === 0);
173 const phase = Number(phaseFilter);
177 filtered = filtered.filter(l => l.f126 >= 0 && l.f126 < 64);
178 } else if (phase === -2) {
179 // All non-active (64+)
180 filtered = filtered.filter(l => l.f126 >= 64);
183 filtered = filtered.filter(l => l.f126 === phase);
189 const term = searchTerm.toLowerCase();
190 filtered = filtered.filter(l =>
191 l.f100.toLowerCase().includes(term) ||
192 l.f101.toLowerCase().includes(term) ||
193 l.f102.toLowerCase().includes(term) ||
194 l.f104.toLowerCase().includes(term) ||
195 l.f114.toLowerCase().includes(term) ||
196 l.f125.toLowerCase().includes(term)
200 setFilteredLanes(filtered);
203 const openNewLaneModal = () => {
204 setEditingLane(null);
220 setShowEditModal(true);
223 const openEditLaneModal = (lane: Lane) => {
224 setEditingLane(lane);
230 f110: lane.f110 === 1,
240 setShowEditModal(true);
243 const saveLane = async () => {
245 if (!editingLane && !laneForm.f100) {
246 alert('A valid unique ID is required');
250 // In production: await apiClient.saveLane(laneForm, editingLane ? 'edit' : 'new')
251 console.log('Saving lane:', laneForm);
252 alert('Lane saved successfully');
253 setShowEditModal(false);
256 console.error('Error saving lane:', error);
257 alert('Failed to save lane');
261 const getLifePhaseName = (phase: number) => {
262 const phases: { [key: number]: string } = {
264 1: 'Back Office Lane',
265 2: 'Head Office Lane',
266 3: 'IT/Support Lane',
267 64: 'Retired/Historic',
271 return phases[phase] || 'Unknown';
274 const formatDateTime = (dateStr: string) => {
275 if (!dateStr) return '';
276 const date = new Date(dateStr);
277 return date.toLocaleString();
282 <div className="p-8">
283 <div className="text-center">Loading lanes...</div>
289 <div className="min-h-screen bg-bg">
291 <div className="bg-surface border-b-2 border-[var(--brand)] p-6">
292 <h1 className="text-3xl font-bold text-text flex items-center gap-2">
293 <Icon name="storefront" size={32} className="text-brand" />
298 <div className="p-6">
300 <div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6">
302 onClick={() => setShowProfileModal(true)}
303 className="bg-surface rounded-lg shadow p-4 hover:shadow-lg transition-shadow flex flex-col items-center justify-center gap-2 border-2 border-transparent hover:border-brand"
305 <Icon name="dashboard" size={40} className="text-brand" />
306 <span className="font-semibold text-text text-sm text-center">Manage Profiles</span>
310 onClick={openNewLaneModal}
311 className="bg-surface rounded-lg shadow p-4 hover:shadow-lg transition-shadow flex flex-col items-center justify-center gap-2 border-2 border-transparent hover:border-brand"
313 <Icon name="add" size={40} className="text-brand" />
314 <span className="font-semibold text-text text-sm text-center">Add New Lane</span>
318 onClick={() => setShowScanModal(true)}
319 className="bg-surface rounded-lg shadow p-4 hover:shadow-lg transition-shadow flex flex-col items-center justify-center gap-2 border-2 border-transparent hover:border-brand"
321 <Icon name="search" size={40} className="text-brand" />
322 <span className="font-semibold text-text text-sm text-center">Scan All Lanes</span>
326 {/* Search and Filters */}
327 <div className="bg-surface rounded-lg shadow p-4 mb-6">
328 <div className="flex flex-wrap items-center gap-4">
329 <div className="flex-1 min-w-[200px]">
332 placeholder="Search lanes..."
334 onChange={(e) => setSearchTerm(e.target.value)}
335 className="w-full px-4 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent"
339 <div className="flex items-center gap-2">
340 <label className="text-sm font-medium text-text">Auth:</label>
343 onChange={(e) => setAuthFilter(e.target.value)}
344 className="px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
346 <option value="0">All Lanes</option>
347 <option value="1">Only Authorised</option>
348 <option value="2">Only Non-Authorised</option>
352 <div className="flex items-center gap-2">
353 <label className="text-sm font-medium text-text">Phase:</label>
356 onChange={(e) => setPhaseFilter(e.target.value)}
357 className="px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
359 <option value="-3">All Lanes Regardless</option>
360 <optgroup label="Active">
361 <option value="-1">All Active Lanes</option>
362 <option value="0">Trading Lanes</option>
363 <option value="1">Store Back Office Lanes</option>
364 <option value="2">Head Office Lanes</option>
365 <option value="3">IT Support Lanes</option>
367 <optgroup label="Not Active">
368 <option value="-2">All Non Active Lanes</option>
369 <option value="64">Retired Lanes</option>
370 <option value="66">Deployed Lanes</option>
371 <option value="65">Test Lanes</option>
376 <div className="text-sm text-muted">
377 Displaying {filteredLanes.length} of {lanes.length}
383 <div className="bg-surface rounded-lg shadow overflow-x-auto">
384 <table className="w-full">
385 <thead className="bg-[var(--brand)] text-surface">
387 <th className="p-3 text-left">ID</th>
388 <th className="p-3 text-left">Last IP</th>
389 <th className="p-3 text-left">Store</th>
390 <th className="p-3 text-left">Name</th>
391 <th className="p-3 text-left">Profile</th>
392 <th className="p-3 text-left">User Interface</th>
393 <th className="p-3 text-left">Asset #</th>
394 <th className="p-3 text-left">Last Seen</th>
395 <th className="p-3 text-left">Options</th>
399 {filteredLanes.map((lane, index) => (
400 <tr key={lane.f100} className={index % 2 === 0 ? 'bg-surface-2' : 'bg-surface'}>
401 <td className="p-3 border-t border-border">
402 <div className="font-semibold text-text">{lane.f100}</div>
403 <div className="text-xs text-muted flex items-center gap-1">
405 <><Icon name="check_circle" size={12} className="text-success" /> Auth</>
407 <><Icon name="cancel" size={12} className="text-danger" /> Not Auth</>
411 <td className="p-3 border-t border-border text-sm">{lane.f114}</td>
412 <td className="p-3 border-t border-border text-sm">{lane.f102}</td>
413 <td className="p-3 border-t border-border font-medium">{lane.f101}</td>
414 <td className="p-3 border-t border-border text-sm">
415 {lane.f128 > 0 ? `Profile ${lane.f128}` : 'None'}
417 <td className="p-3 border-t border-border text-sm">
418 {lane.uistr || lane.f129 || 'Default'}
420 <td className="p-3 border-t border-border text-sm">{lane.f125}</td>
421 <td className="p-3 border-t border-border text-sm">{formatDateTime(lane.f107)}</td>
422 <td className="p-3 border-t border-border">
423 <div className="flex gap-2">
425 onClick={() => openEditLaneModal(lane)}
426 className="px-3 py-1 bg-info text-surface text-sm rounded hover:bg-info/80 flex items-center gap-1"
428 <Icon name="edit" size={14} />
432 href={`/pages/settings/staff?lane=${lane.f100}`}
433 className="px-3 py-1 bg-brand text-surface text-sm rounded hover:bg-brand/90 flex items-center gap-1"
435 <Icon name="assignment" size={14} />
445 {filteredLanes.length === 0 && (
446 <div className="p-8 text-center text-muted">
447 No lanes found matching your criteria
453 {/* Edit/New Lane Modal */}
455 <div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4">
456 <div className="bg-surface/95 backdrop-blur-md rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
457 <div className="sticky top-0 bg-[var(--brand)] text-surface p-4 flex justify-between items-center">
458 <h2 className="text-xl font-bold">{editingLane ? 'Edit Lane' : 'Add New Lane'}</h2>
460 onClick={() => setShowEditModal(false)}
461 className="text-surface hover:text-muted/70 text-2xl font-bold"
463 <Icon name="close" size={24} />
467 <div className="p-6">
468 <div className="space-y-4">
469 <div className="grid grid-cols-2 gap-4">
471 <label className="block text-sm font-medium text-text mb-1">
472 Unique ID {!editingLane && <span className="text-danger">*</span>}
476 value={laneForm.f100}
477 onChange={(e) => setLaneForm({...laneForm, f100: e.target.value})}
478 disabled={!!editingLane}
479 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent disabled:bg-surface-2"
484 <label className="flex items-center gap-2 cursor-pointer">
487 checked={laneForm.f110}
488 onChange={(e) => setLaneForm({...laneForm, f110: e.target.checked})}
489 className="w-5 h-5 text-[var(--brand)] focus:ring-[var(--brand)]"
491 <span className="text-sm font-medium text-text">Authorised</span>
497 <label className="block text-sm font-medium text-text mb-1">Name</label>
500 value={laneForm.f101}
501 onChange={(e) => setLaneForm({...laneForm, f101: e.target.value})}
502 placeholder="e.g., POS1"
503 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
505 <p className="text-xs text-muted mt-1">A brief name for this lane</p>
509 <label className="block text-sm font-medium text-text mb-1">Store Name</label>
512 value={laneForm.f104}
513 onChange={(e) => setLaneForm({...laneForm, f104: e.target.value})}
514 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
516 <p className="text-xs text-muted mt-1">Optional, only used for some reports</p>
519 <div className="grid grid-cols-2 gap-4">
521 <label className="block text-sm font-medium text-text mb-1">Location</label>
524 value={laneForm.f102}
525 onChange={(e) => setLaneForm({...laneForm, f102: e.target.value})}
526 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
531 <label className="block text-sm font-medium text-text mb-1">Profile</label>
533 value={laneForm.f128}
534 onChange={(e) => setLaneForm({...laneForm, f128: Number(e.target.value)})}
535 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
537 <option value={0}>None</option>
539 <option key={p.id} value={p.id}>
540 {p.name || `Profile ${p.id}`}
548 <label className="block text-sm font-medium text-text mb-1">Automatic Code Update</label>
550 value={laneForm.f132}
551 onChange={(e) => setLaneForm({...laneForm, f132: e.target.value})}
552 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
554 <option value="0">Inherit from profile</option>
555 <option value="manual">Manual</option>
556 <option value="latest">Latest</option>
557 <option value="stable">Stable</option>
558 <option value="dev">Development</option>
559 <option value="whitehot">Whitehot</option>
560 <option value="longterm">Previous Generation</option>
565 <label className="block text-sm font-medium text-text mb-1">
566 Canary Level: {laneForm.f131}
572 value={laneForm.f131}
573 onChange={(e) => setLaneForm({...laneForm, f131: Number(e.target.value)})}
576 <p className="text-xs text-muted mt-1">
577 Controls how aggressive updates are applied. Higher = earlier updates.
582 <label className="block text-sm font-medium text-text mb-1">User Interface #</label>
585 value={laneForm.f130}
586 onChange={(e) => setLaneForm({...laneForm, f130: e.target.value})}
587 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
592 <label className="block text-sm font-medium text-text mb-1">Asset Number</label>
595 value={laneForm.f125}
596 onChange={(e) => setLaneForm({...laneForm, f125: e.target.value})}
597 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
602 <label className="block text-sm font-medium text-text mb-1">Comments</label>
605 value={laneForm.f124}
606 onChange={(e) => setLaneForm({...laneForm, f124: e.target.value})}
607 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
612 <label className="block text-sm font-medium text-text mb-1">IP Address</label>
615 value={laneForm.f114}
616 onChange={(e) => setLaneForm({...laneForm, f114: e.target.value})}
617 placeholder="e.g., 192.168.1.100"
618 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
620 <p className="text-xs text-muted mt-1">Optional, will be updated when it connects</p>
624 <label className="block text-sm font-medium text-text mb-1">Life Phase</label>
626 value={laneForm.f126}
627 onChange={(e) => setLaneForm({...laneForm, f126: Number(e.target.value)})}
628 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
630 <optgroup label="Active">
631 <option value={0}>Trading Lane</option>
632 <option value={1}>Back Office Lane</option>
633 <option value={2}>Head Office Lane</option>
634 <option value={3}>IT/Support Lane</option>
636 <optgroup label="Non Active">
637 <option value={64}>Retired/Historic</option>
638 <option value={65}>Test Lane</option>
639 <option value={66}>Deployed Lane</option>
645 <div className="mt-6 flex gap-3">
648 className="flex-1 px-6 py-3 bg-[var(--brand)] text-surface rounded-lg hover:bg-[var(--brand2)] transition-colors font-semibold flex items-center justify-center gap-2"
650 <Icon name="save" size={20} />
654 onClick={() => setShowEditModal(false)}
655 className="px-6 py-3 bg-surface-2 text-text rounded-lg hover:bg-surface-2/80 transition-colors"
665 {/* Profile Management Modal */}
666 {showProfileModal && (
667 <div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4">
668 <div className="bg-surface/95 backdrop-blur-md rounded-lg shadow-2xl max-w-3xl w-full">
669 <div className="bg-[var(--brand)] text-surface p-4 flex justify-between items-center rounded-t-lg">
670 <h2 className="text-xl font-bold">Manage Profiles</h2>
672 onClick={() => setShowProfileModal(false)}
673 className="text-surface hover:text-muted/70 text-2xl font-bold"
675 <Icon name="close" size={24} />
679 <div className="p-6">
680 <p className="text-text mb-6" style={{maxWidth: '26em'}}>
681 A profile is a grouping of lanes that share similar attributes and should be configured the same way.
682 Typically all selling counters might belong to one profile and all backoffice systems to another profile.
683 Several of the configuration options allow you to configure based on profile.
686 <table className="w-full border-collapse">
688 <tr className="bg-surface-2">
689 <th className="p-3 text-center border border-border">Profile Number</th>
690 <th className="p-3 text-center border border-border">Profile Name</th>
691 <th className="p-3 text-center border border-border">Count Using this profile</th>
695 {profiles.map((profile) => (
696 <tr key={profile.id}>
697 <td className="p-3 text-center border border-border font-semibold">{profile.id}</td>
698 <td className="p-3 border border-border">
704 const updated = profiles.map(p =>
705 p.id === profile.id ? {...p, name: e.target.value} : p
707 setProfiles(updated);
709 onBlur={async (e) => {
710 // Save profile name on blur
711 const name = e.target.value || `Profile ${profile.id}`;
713 // In production: await apiClient.saveProfileName(profile.id, name)
714 console.log(`Saving profile ${profile.id} name:`, name);
716 console.error('Error saving profile name:', error);
719 className="w-full px-2 py-1 border border-border rounded focus:ring-1 focus:ring-[var(--brand)] focus:border-transparent"
722 <td className="p-3 text-center border border-border">{profile.used}</td>
726 <td colSpan={2} className="p-3 text-center border border-border font-semibold">
727 Lanes not assigned to any profile
729 <td className="p-3 text-center border border-border">0</td>
738 {/* Scan Lanes Modal */}
740 <div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4">
741 <div className="bg-surface/95 backdrop-blur-md rounded-lg shadow-2xl max-w-2xl w-full">
742 <div className="bg-[var(--brand)] text-surface p-4 flex justify-between items-center rounded-t-lg">
743 <h2 className="text-xl font-bold">Scan All Lanes</h2>
745 onClick={() => setShowScanModal(false)}
746 className="text-surface hover:text-muted/70 text-2xl font-bold"
748 <Icon name="close" size={24} />
752 <div className="p-6">
753 <h3 className="text-lg font-semibold mb-4">PosGreen Lanes</h3>
755 <div className="space-y-4">
757 <label className="block text-sm font-medium text-text mb-1">
758 Retrieve Current Setting
762 placeholder="Setting name"
763 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
768 <label className="block text-sm font-medium text-text mb-1">
769 Database Table Row Estimate
773 placeholder="Table name"
774 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
779 <label className="block text-sm font-medium text-text mb-1">
785 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-[var(--brand)] focus:border-transparent"
790 onClick={() => alert('Scan functionality will be implemented with API integration')}
791 className="w-full px-6 py-3 bg-[var(--brand)] text-surface rounded-lg hover:bg-[var(--brand2)] transition-colors font-semibold flex items-center justify-center gap-2"
793 <Icon name="add_circle" size={20} />