Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import { Link, useLocation } from 'react-router-dom'; | |
| import { | |
| Home, | |
| FileInput, | |
| FileOutput, | |
| Users, | |
| Package, | |
| Settings, | |
| Menu, | |
| X, | |
| TrendingUp | |
| } from 'lucide-react'; | |
| interface LayoutProps { | |
| children: React.ReactNode; | |
| } | |
| const Layout: React.FC<LayoutProps> = ({ children }) => { | |
| const [isSidebarOpen, setSidebarOpen] = useState(false); | |
| const location = useLocation(); | |
| const [pigmiPopup, setPigmiPopup] = useState<{ open: boolean; message: string }>(() => ({ | |
| open: false, | |
| message: '', | |
| })); | |
| const getPigmiState = () => { | |
| try { | |
| const stored = localStorage.getItem('pp_pigmi_config'); | |
| if (!stored) return null; | |
| const parsed = JSON.parse(stored); | |
| const enabled = typeof parsed.enabled === 'boolean' ? parsed.enabled : false; | |
| const message = typeof parsed.message === 'string' ? parsed.message : ''; | |
| const time = typeof parsed.time === 'string' && parsed.time ? parsed.time : '09:00'; | |
| const dayOfMonth = | |
| typeof parsed.dayOfMonth === 'number' && | |
| !Number.isNaN(parsed.dayOfMonth) && | |
| parsed.dayOfMonth >= 1 && | |
| parsed.dayOfMonth <= 31 | |
| ? parsed.dayOfMonth | |
| : 1; | |
| if (!enabled || !message.trim()) return null; | |
| const now = new Date(); | |
| const key = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; | |
| const lastKey = localStorage.getItem('pp_pigmi_last_shown_month') || ''; | |
| const [hh, mm] = String(time).split(':').map((v: string) => Number(v)); | |
| const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate(); | |
| const day = Math.max(1, Math.min(lastDay, dayOfMonth)); | |
| const scheduled = new Date(now.getFullYear(), now.getMonth(), day, hh || 0, mm || 0, 0, 0); | |
| return { | |
| key, | |
| lastKey, | |
| message: message.trim(), | |
| dueNow: now.getTime() >= scheduled.getTime(), | |
| alreadyShownThisMonth: lastKey === key, | |
| }; | |
| } catch (error) { | |
| console.error('Error reading pigmi config', error); | |
| return null; | |
| } | |
| }; | |
| const triggerPigmiIfDue = () => { | |
| const pigmi = getPigmiState(); | |
| if (!pigmi) return; | |
| if (pigmi.dueNow && !pigmi.alreadyShownThisMonth) { | |
| try { | |
| localStorage.setItem('pp_pigmi_last_shown_month', pigmi.key); | |
| } catch (error) { | |
| console.error('Error saving pigmi month key', error); | |
| } | |
| setPigmiPopup({ open: true, message: pigmi.message }); | |
| } | |
| }; | |
| useEffect(() => { | |
| triggerPigmiIfDue(); | |
| const id = window.setInterval(() => triggerPigmiIfDue(), 30_000); | |
| return () => window.clearInterval(id); | |
| }, []); | |
| const navItems = [ | |
| { path: '/', label: 'डॅशबोर्ड (Home)', icon: Home }, | |
| { path: '/jawaak', label: 'जावक बिल (Sales)', icon: FileOutput }, | |
| { path: '/awaak', label: 'आवक बिल (Purchase)', icon: FileInput }, | |
| { path: '/ledger', label: 'पार्टी लेजर (Ledger)', icon: Users }, | |
| { path: '/stock', label: 'स्टॉक रिपोर्ट (Stock)', icon: Package }, | |
| { path: '/analysis', label: 'अॅनालिसीस (Analysis)', icon: TrendingUp }, | |
| { path: '/settings', label: 'सेटिंग्स (Settings)', icon: Settings }, | |
| ]; | |
| const isActive = (path: string) => location.pathname === path; | |
| return ( | |
| <div className="flex h-screen bg-gray-50 overflow-hidden"> | |
| {pigmiPopup.open && ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> | |
| <div | |
| className="absolute inset-0 bg-black/40" | |
| onClick={() => setPigmiPopup((p) => ({ ...p, open: false }))} | |
| /> | |
| <div className="relative w-full max-w-md bg-white rounded-xl shadow-2xl border border-gray-100 overflow-hidden"> | |
| <div className="p-4 bg-teal-700 text-white flex items-center justify-between"> | |
| <div className="font-semibold">Pigmi Reminder</div> | |
| <button | |
| className="text-white/90 hover:text-white text-sm font-medium" | |
| onClick={() => setPigmiPopup((p) => ({ ...p, open: false }))} | |
| > | |
| Close | |
| </button> | |
| </div> | |
| <div className="p-4"> | |
| <div className="text-sm text-gray-700 whitespace-pre-wrap">{pigmiPopup.message}</div> | |
| <div className="mt-4 flex justify-end"> | |
| <button | |
| className="px-4 py-2 bg-teal-600 text-white rounded-lg text-sm font-medium hover:bg-teal-700" | |
| onClick={() => setPigmiPopup((p) => ({ ...p, open: false }))} | |
| > | |
| OK | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Mobile Sidebar Overlay */} | |
| {isSidebarOpen && ( | |
| <div | |
| className="fixed inset-0 bg-black bg-opacity-50 z-20 lg:hidden" | |
| onClick={() => setSidebarOpen(false)} | |
| /> | |
| )} | |
| {/* Sidebar */} | |
| <aside | |
| className={`fixed lg:static inset-y-0 left-0 w-64 bg-teal-800 text-white transform transition-transform duration-200 ease-in-out z-30 ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0' | |
| }`} | |
| > | |
| <div className="flex items-center justify-between p-4 border-b border-teal-700 h-16"> | |
| <div className="flex items-center gap-2"> | |
| <TrendingUp className="w-6 h-6 text-teal-300" /> | |
| <span className="font-bold text-xl">Mirchi Vyapar</span> | |
| </div> | |
| <button | |
| className="lg:hidden p-1 hover:bg-teal-700 rounded" | |
| onClick={() => setSidebarOpen(false)} | |
| > | |
| <X size={24} /> | |
| </button> | |
| </div> | |
| <nav className="p-4 space-y-1"> | |
| {navItems.map((item) => ( | |
| <Link | |
| key={item.path} | |
| to={item.path} | |
| onClick={() => setSidebarOpen(false)} | |
| className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(item.path) | |
| ? 'bg-teal-900 text-teal-100 shadow-sm' | |
| : 'text-teal-100 hover:bg-teal-700' | |
| }`} | |
| > | |
| <item.icon size={20} /> | |
| <span className="font-medium">{item.label}</span> | |
| </Link> | |
| ))} | |
| </nav> | |
| <div className="absolute bottom-0 w-full p-4 border-t border-teal-700 bg-teal-800"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 rounded-full bg-teal-600 flex items-center justify-center font-bold"> | |
| A | |
| </div> | |
| <div> | |
| <p className="text-sm font-medium">Admin User</p> | |
| <p className="text-xs text-teal-300">Mirchi Mandi</p> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| {/* Main Content */} | |
| <main className="flex-1 flex flex-col overflow-hidden"> | |
| {/* Top Header */} | |
| <header className="h-16 bg-white border-b flex items-center justify-between px-4 lg:px-8"> | |
| <button | |
| className="lg:hidden p-2 -ml-2 text-gray-600 hover:bg-gray-100 rounded-lg" | |
| onClick={() => setSidebarOpen(true)} | |
| > | |
| <Menu size={24} /> | |
| </button> | |
| <h1 className="text-xl font-semibold text-gray-800 ml-2 lg:ml-0"> | |
| {navItems.find(i => isActive(i.path))?.label.split(' (')[0] || 'Mirchi Vyapar'} | |
| </h1> | |
| <div className="flex items-center gap-4"> | |
| <span className="text-sm text-gray-500 hidden md:block"> | |
| {new Date().toLocaleDateString('mr-IN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} | |
| </span> | |
| </div> | |
| </header> | |
| {/* Page Content */} | |
| <div className="flex-1 overflow-auto p-4 lg:p-6 pb-20 lg:pb-6"> | |
| {children} | |
| </div> | |
| </main> | |
| </div> | |
| ); | |
| }; | |
| export default Layout; |