Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,573 changes: 2,314 additions & 259 deletions pos/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react-dom": "^19.0.0",
"react-router-dom": "^6.30.1",
"react-toastify": "^11.0.5",
"recharts": "^3.7.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^3.4.17",
"uuid": "^11.1.0",
Expand Down
6 changes: 4 additions & 2 deletions pos/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Header from './components/Header';
import Orders from './pages/Orders';
import POS from './pages/POS';
import Table from './pages/Table';
import Dashboard from './pages/Dashboard';
import AuthGuard from './components/AuthGuard';
import POSOpeningProvider from './components/POSOpeningProvider';
import ScreenSizeProvider from './components/ScreenSizeProvider';
Expand All @@ -15,7 +16,7 @@ function App() {
const {
initializeApp
} = usePOSStore();

useEffect(() => {
initializeApp();
}, [initializeApp]);
Expand All @@ -30,9 +31,10 @@ function App() {
<Header />
<div className="flex-1 overflow-hidden">
<Routes>
<Route path="/" element={<POS/>} />
<Route path="/" element={<POS />} />
<Route path="/orders" element={<Orders />} />
<Route path="/table" element={<Table />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</div>
<Footer />
Expand Down
47 changes: 30 additions & 17 deletions pos/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import {
import { Link, useLocation, useNavigate } from 'react-router-dom';
import {
Command,
User,
ChevronDown,
Monitor,
LogOut,
RefreshCw,
LayoutDashboard,
} from 'lucide-react';
import { Button, Input } from './ui';
import { useRootStore } from '../store/root-store';
Expand All @@ -21,6 +22,7 @@ const Header = () => {
const user = useRootStore((state: RootState) => state.user);
const searchInputRef = useRef<HTMLInputElement>(null);
const location = useLocation();
const navigate = useNavigate();
const { searchQuery, setSearchQuery } = usePOSStore();
const { orderSearchQuery, setOrderSearchQuery } = useRootStore();
const [orderSearchInput, setOrderSearchInput] = useState(orderSearchQuery);
Expand Down Expand Up @@ -107,28 +109,28 @@ const Header = () => {
<div className="flex items-center justify-between h-16 px-6">
{/* Logo */}
<div className="flex items-center">
<Link to="/" className="flex items-center space-x-3">
<img
src="/assets/ury/pos/ury_pos.png"
alt="URY POS"
<Link to="/" className="flex items-center space-x-3">
<img
src="/assets/ury/pos/ury_pos.png"
alt="URY POS"
className="h-10 w-auto"
/>
</Link>
</div>

{/* Search Bar */}
<div className="px-4 py-2 flex-1 flex items-center max-w-2xl mx-8 bg-gray-50 hover:bg-gray-100 border border-input rounded-md">
<Input
ref={searchInputRef}
placeholder={searchPlaceholder}
className="h-fit p-0 w-full bg-transparent border-0 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
value={searchValue}
onChange={searchOnChange}
/>
<div className="flex items-center gap-2 text-gray-400">
<Command className="w-4 h-4" />
<span>K</span>
</div>
<Input
ref={searchInputRef}
placeholder={searchPlaceholder}
className="h-fit p-0 w-full bg-transparent border-0 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
value={searchValue}
onChange={searchOnChange}
/>
<div className="flex items-center gap-2 text-gray-400">
<Command className="w-4 h-4" />
<span>K</span>
</div>
</div>

{/* Right side actions */}
Expand Down Expand Up @@ -163,6 +165,17 @@ const Header = () => {
<Monitor className="w-4 h-4 mr-3" />
Switch To Desk
</Button>
<Button
variant="ghost"
className="flex justify-start items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
onClick={() => {
navigate('/dashboard');
setShowUserMenu(false);
}}
>
<LayoutDashboard className="w-4 h-4 mr-3" />
Dashboard
</Button>
<Button
variant="ghost"
className="flex justify-start items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
Expand Down
68 changes: 68 additions & 0 deletions pos/src/components/dashboard/BestSellingItems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
import { DashboardData } from '../../lib/dashboard-api';
import { DashboardFilters } from '../../pages/Dashboard';
import GenericChart from './charts/GenericChart';

interface Props {
data: DashboardData | null;
loading: boolean;
filters: DashboardFilters;
}

const BestSellingItems: React.FC<Props> = ({ data: initialData, filters: globalFilters }) => {
// BestSellingItems expects DashboardData object in data prop but uses data.bestSellingItems
// GenericChart will handle fetching based on 'best_selling_items' key.

// Initial data passed in might be null or DashboardData.
const initialItems = initialData?.bestSellingItems || [];

return (
<GenericChart
title="Top 10 Best Selling Items"
initialData={initialItems}
globalFilters={globalFilters}
apiSection="best_selling_items"
mapData={(data) => (data || [])
.slice(0, 10)
.map((item: any) => ({
name: item.item_name,
value: item.total_revenue
}))
}
renderChart={(chartData) => (
<BarChart
data={chartData}
layout="vertical"
margin={{ left: 0, right: 20, bottom: 20 }}
>
<CartesianGrid strokeDasharray="3 3" horizontal={false} stroke="#f1f5f9" />
<XAxis type="number" hide />
<YAxis
type="category"
dataKey="name"
fontSize={12}
tickLine={false}
axisLine={false}
tick={{ fill: '#64748b' }}
width={150}
/>
<Tooltip
cursor={{ fill: '#f8fafc' }}
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
formatter={(value: any) => `₹${(value || 0).toLocaleString()}`}
/>
<Bar
dataKey="value"
fill="#f59e0b"
radius={[0, 4, 4, 0]}
barSize={32}
name="Revenue"
/>
</BarChart>
)}
/>
);
};

export default BestSellingItems;
25 changes: 25 additions & 0 deletions pos/src/components/dashboard/Charts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { DashboardFilters } from '../../pages/Dashboard';
import { DashboardData } from '../../lib/dashboard-api';
import SalesTrendChart from './charts/SalesTrendChart';
import PaymentModeChart from './charts/PaymentModeChart';
import CategorySalesChart from './charts/CategorySalesChart';
import PeakHoursChart from './charts/PeakHoursChart';

interface Props {
data: DashboardData | null;
filters: DashboardFilters;
}

const Charts: React.FC<Props> = ({ data, filters }) => {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<SalesTrendChart initialData={data?.daywise_sales || []} globalFilters={filters} />
<PaymentModeChart initialData={data?.payment_modes || []} globalFilters={filters} />
<CategorySalesChart initialData={data?.sales_by_category || []} globalFilters={filters} />
<PeakHoursChart initialData={data?.peak_hours || []} globalFilters={filters} />
</div>
);
};

export default Charts;
87 changes: 87 additions & 0 deletions pos/src/components/dashboard/FilterBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import { Select, SelectItem } from '../ui/select';
import { DashboardFilters } from '../../pages/Dashboard';
import { Card } from '../ui/card';
import { Input } from '../ui/input';

interface Props {
filters: DashboardFilters;
onFilterChange: (filters: DashboardFilters) => void;
branches: string[];
}

const FilterBar: React.FC<Props & { variant?: 'default' | 'clean' }> = ({
filters,
onFilterChange,
branches,
variant = 'default'
}) => {
const handleDateRangeChange = (value: string) => {
onFilterChange({ ...filters, dateRange: value as any });
};

const content = (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500 uppercase">Date Range</label>
<Select
value={filters.dateRange}
onValueChange={handleDateRangeChange}
>
<SelectItem value="today">Today</SelectItem>
<SelectItem value="this_week">This Week</SelectItem>
<SelectItem value="this_month">This Month</SelectItem>
<SelectItem value="custom">Custom Range</SelectItem>
</Select>
</div>

{filters.dateRange === 'custom' && (
<div className="grid grid-cols-2 gap-4 col-span-1 md:col-span-2 lg:col-span-2">
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500 uppercase">From</label>
<Input
type="date"
value={filters.customStartDate || ''}
onChange={(e) => onFilterChange({ ...filters, customStartDate: e.target.value })}
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500 uppercase">To</label>
<Input
type="date"
value={filters.customEndDate || ''}
onChange={(e) => onFilterChange({ ...filters, customEndDate: e.target.value })}
/>
</div>
</div>
)}

{branches.length > 0 && (
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500 uppercase">Branch</label>
<Select
value={filters.branch || 'all'}
onValueChange={(val) => onFilterChange({ ...filters, branch: val === 'all' ? undefined : val })}
>
<SelectItem value="all">All Branches</SelectItem>
{branches.map(branch => (
<SelectItem key={branch} value={branch}>{branch}</SelectItem>
))}
</Select>
</div>
)}
</div>
);

if (variant === 'clean') {
return content;
}

return (
<Card className="p-4 bg-white border-gray-200">
{content}
</Card>
);
};

export default FilterBar;
Loading
Loading