feat: Phase 2 feature flags — hide/show sidebar entries per client
- usePhase2Features.js hook: manages localStorage-based feature flags for all 8 Phase 2 sidebar entries (notes, expenses, contacts, customers, tabs, waste, kds, schedule) All default to enabled=true. setFeatureEnabled() writes to localStorage and dispatches a 'phase2features' event so all subscribers re-render immediately without page reload - Phase2FeaturesTab.jsx: new Settings tab 'Λειτουργίες' — toggle each Phase 2 page on/off with master 'Ενεργοποίηση όλων' / 'Απενεργοποίηση όλων' buttons Note shown: changes are localStorage-only, instant, no reload needed, no data deleted - SettingsPage.jsx: adds 'Λειτουργίες' and reinstates 'Developer' tab (was missing import) - Sidebar.jsx: subscribes to phase2features + storage events; filters ALL_NAV to remove disabled Phase 2 entries; each phase2 item has a phase2 id that isFeatureEnabled() checks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,15 @@ import { NavLink } from 'react-router-dom'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { BarChart2, LayoutGrid, ClipboardList, Package, Settings, ChevronRight, ChevronLeft, ShoppingBag, NotebookPen, Receipt, BookUser, Users, CreditCard, Trash2, ChefHat, CalendarDays } from 'lucide-react'
|
||||
import { getIncomingOrders } from '../api/client'
|
||||
import { isFeatureEnabled } from '../hooks/usePhase2Features'
|
||||
|
||||
// Phase 2 feature IDs mapped to their sidebar routes (must match usePhase2Features defs)
|
||||
const PHASE2_ROUTES = new Set(['/notes', '/expenses', '/contacts', '/customers', '/tabs', '/waste', '/kds', '/schedule'])
|
||||
|
||||
export default function Sidebar() {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [pendingCount, setPendingCount] = useState(0)
|
||||
const [, featTick] = useState(0)
|
||||
const pollRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -20,23 +25,37 @@ export default function Sidebar() {
|
||||
return () => clearInterval(pollRef.current)
|
||||
}, [])
|
||||
|
||||
const NAV = [
|
||||
// Re-render when feature flags change
|
||||
useEffect(() => {
|
||||
const handler = () => featTick(n => n + 1)
|
||||
window.addEventListener('phase2features', handler)
|
||||
window.addEventListener('storage', handler)
|
||||
return () => {
|
||||
window.removeEventListener('phase2features', handler)
|
||||
window.removeEventListener('storage', handler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const ALL_NAV = [
|
||||
{ to: '/dashboard', icon: BarChart2, label: 'Dashboard' },
|
||||
{ to: '/tables', icon: LayoutGrid, label: 'Τραπέζια' },
|
||||
{ to: '/online-orders', icon: ShoppingBag, label: 'Online Orders', badge: pendingCount },
|
||||
{ to: '/reports', icon: ClipboardList, label: 'Αναφορές' },
|
||||
{ to: '/management', icon: Package, label: 'Διαχείριση' },
|
||||
{ to: '/notes', icon: NotebookPen, label: 'Σημειώσεις' },
|
||||
{ to: '/expenses', icon: Receipt, label: 'Έξοδα' },
|
||||
{ to: '/contacts', icon: BookUser, label: 'Επαφές' },
|
||||
{ to: '/customers', icon: Users, label: 'Πελάτες' },
|
||||
{ to: '/tabs', icon: CreditCard, label: 'Καρτέλες' },
|
||||
{ to: '/waste', icon: Trash2, label: 'Αποβλήτα' },
|
||||
{ to: '/kds', icon: ChefHat, label: 'KDS' },
|
||||
{ to: '/schedule', icon: CalendarDays, label: 'Πρόγραμμα' },
|
||||
{ to: '/notes', icon: NotebookPen, label: 'Σημειώσεις', phase2: 'notes' },
|
||||
{ to: '/expenses', icon: Receipt, label: 'Έξοδα', phase2: 'expenses' },
|
||||
{ to: '/contacts', icon: BookUser, label: 'Επαφές', phase2: 'contacts' },
|
||||
{ to: '/customers', icon: Users, label: 'Πελάτες', phase2: 'customers' },
|
||||
{ to: '/tabs', icon: CreditCard, label: 'Καρτέλες', phase2: 'tabs' },
|
||||
{ to: '/waste', icon: Trash2, label: 'Αποβλήτα', phase2: 'waste' },
|
||||
{ to: '/kds', icon: ChefHat, label: 'KDS', phase2: 'kds' },
|
||||
{ to: '/schedule', icon: CalendarDays, label: 'Πρόγραμμα', phase2: 'schedule' },
|
||||
{ to: '/settings', icon: Settings, label: 'Ρυθμίσεις' },
|
||||
]
|
||||
|
||||
// Filter out disabled Phase 2 entries
|
||||
const NAV = ALL_NAV.filter(item => !item.phase2 || isFeatureEnabled(item.phase2))
|
||||
|
||||
return (
|
||||
<aside className={`flex flex-col bg-primary-800 text-white shrink-0 transition-all duration-200 ${collapsed ? 'w-16' : 'w-56'}`}>
|
||||
{/* Logo / collapse toggle */}
|
||||
|
||||
60
manager_dashboard/src/hooks/usePhase2Features.js
Normal file
60
manager_dashboard/src/hooks/usePhase2Features.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
const STORAGE_KEY = 'phase2_features'
|
||||
|
||||
// All Phase 2 features with their sidebar metadata.
|
||||
// enabled: true = shown by default (feature is polished)
|
||||
// enabled: false = hidden by default (still in development)
|
||||
export const PHASE2_FEATURE_DEFS = [
|
||||
{ id: 'notes', label: 'Σημειώσεις & Εργασίες', route: '/notes', enabled: true },
|
||||
{ id: 'expenses', label: 'Έξοδα', route: '/expenses', enabled: true },
|
||||
{ id: 'contacts', label: 'Επαφές / Προμηθευτές', route: '/contacts', enabled: true },
|
||||
{ id: 'customers', label: 'Πελάτες', route: '/customers',enabled: true },
|
||||
{ id: 'tabs', label: 'Καρτέλες', route: '/tabs', enabled: true },
|
||||
{ id: 'waste', label: 'Αποβλήτα / Φθορές', route: '/waste', enabled: true },
|
||||
{ id: 'kds', label: 'KDS — Κουζίνα', route: '/kds', enabled: true },
|
||||
{ id: 'schedule', label: 'Πρόγραμμα Βαρδιών', route: '/schedule', enabled: true },
|
||||
]
|
||||
|
||||
function _load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
return raw ? JSON.parse(raw) : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function isFeatureEnabled(id) {
|
||||
const overrides = _load()
|
||||
if (id in overrides) return overrides[id]
|
||||
const def = PHASE2_FEATURE_DEFS.find(f => f.id === id)
|
||||
return def ? def.enabled : true
|
||||
}
|
||||
|
||||
export function setFeatureEnabled(id, value) {
|
||||
const overrides = _load()
|
||||
overrides[id] = value
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides))
|
||||
// Notify other components in this tab
|
||||
window.dispatchEvent(new Event('phase2features'))
|
||||
}
|
||||
|
||||
export function usePhase2Features() {
|
||||
const [, tick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => tick(n => n + 1)
|
||||
window.addEventListener('phase2features', handler)
|
||||
window.addEventListener('storage', handler)
|
||||
return () => {
|
||||
window.removeEventListener('phase2features', handler)
|
||||
window.removeEventListener('storage', handler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return PHASE2_FEATURE_DEFS.map(f => ({
|
||||
...f,
|
||||
enabled: isFeatureEnabled(f.id),
|
||||
}))
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import ColoursTab from './tabs/ColoursTab'
|
||||
import OperationTab from './tabs/OperationTab'
|
||||
import PrintFontsTab from './tabs/PrintFontsTab'
|
||||
import SecurityTab from './tabs/SecurityTab'
|
||||
import DevelopmentTab from './tabs/DevelopmentTab'
|
||||
import Phase2FeaturesTab from './tabs/Phase2FeaturesTab'
|
||||
import { TabGroup } from '../../ui/Tabs'
|
||||
|
||||
const TABS = [
|
||||
@@ -12,6 +14,8 @@ const TABS = [
|
||||
{ id: 'operation', label: 'Λειτουργία' },
|
||||
{ id: 'colours', label: 'Εμφάνιση' },
|
||||
{ id: 'print-fonts', label: 'Εκτύπωση' },
|
||||
{ id: 'features', label: 'Λειτουργίες' },
|
||||
{ id: 'development', label: 'Developer' },
|
||||
]
|
||||
|
||||
export default function SettingsPage() {
|
||||
@@ -26,6 +30,8 @@ export default function SettingsPage() {
|
||||
{activeTab === 'operation' && <OperationTab />}
|
||||
{activeTab === 'colours' && <ColoursTab />}
|
||||
{activeTab === 'print-fonts' && <PrintFontsTab />}
|
||||
{activeTab === 'features' && <Phase2FeaturesTab />}
|
||||
{activeTab === 'development' && <DevelopmentTab />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { PHASE2_FEATURE_DEFS, isFeatureEnabled, setFeatureEnabled, usePhase2Features } from '../../../hooks/usePhase2Features'
|
||||
|
||||
function Toggle({ checked, onChange }) {
|
||||
return (
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={() => onChange(!checked)}
|
||||
style={{
|
||||
width: 44, height: 24, borderRadius: 999, border: 'none', cursor: 'pointer',
|
||||
background: checked ? '#16a34a' : '#d1d5db',
|
||||
position: 'relative', transition: 'background 150ms', flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
position: 'absolute', top: 3, left: checked ? 23 : 3,
|
||||
width: 18, height: 18, borderRadius: '50%', background: 'white',
|
||||
transition: 'left 150ms', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
||||
}} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Phase2FeaturesTab() {
|
||||
const features = usePhase2Features()
|
||||
|
||||
const allOn = features.every(f => f.enabled)
|
||||
const allOff = features.every(f => !f.enabled)
|
||||
|
||||
function toggleAll(value) {
|
||||
PHASE2_FEATURE_DEFS.forEach(f => setFeatureEnabled(f.id, value))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-xl">
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-xs text-blue-800">
|
||||
Αυτές οι επιλογές ελέγχουν ποιες σελίδες Phase 2 εμφανίζονται στο πλαϊνό μενού.
|
||||
Απενεργοποίηση <strong>δεν σβήνει δεδομένα</strong> — απλώς κρύβει τον σύνδεσμο.
|
||||
Χρήσιμο για να κρατάτε σελίδες σε εξέλιξη κρυφές από πελάτες.
|
||||
</div>
|
||||
|
||||
<div className="card divide-y divide-gray-100">
|
||||
{/* Master toggle row */}
|
||||
<div className="flex items-center justify-between gap-4 px-5 py-3 bg-gray-50/60">
|
||||
<span className="text-sm font-semibold text-gray-600">Όλες οι λειτουργίες</span>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => toggleAll(true)}
|
||||
disabled={allOn}
|
||||
className="text-xs font-semibold px-3 py-1 rounded-full border border-green-300 text-green-700 hover:bg-green-50 disabled:opacity-40 disabled:cursor-default"
|
||||
>
|
||||
Ενεργοποίηση όλων
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleAll(false)}
|
||||
disabled={allOff}
|
||||
className="text-xs font-semibold px-3 py-1 rounded-full border border-red-300 text-red-600 hover:bg-red-50 disabled:opacity-40 disabled:cursor-default"
|
||||
>
|
||||
Απενεργοποίηση όλων
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{features.map(f => (
|
||||
<div key={f.id} className="flex items-center justify-between gap-4 px-5 py-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700">{f.label}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 font-mono">{f.route}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={f.enabled}
|
||||
onChange={val => setFeatureEnabled(f.id, val)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-400">
|
||||
Οι ρυθμίσεις αποθηκεύονται τοπικά στον browser. Ισχύουν αμέσως χωρίς ανανέωση.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user