diff --git a/CLAUDE.md b/CLAUDE.md index 498fd933..9c7ba454 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -357,18 +357,22 @@ src/ │ ├── admin/ # Admin-specific components │ ├── operator/ # Operator terminal components │ ├── ui/ # shadcn/ui base components -│ └── shared/ # Cross-cutting components -├── pages/ # Route pages +│ └── Layout.tsx # Role-based layout router +├── pages/ # Route pages (with barrel exports) │ ├── admin/ # Admin pages -│ ├── operator/ # Operator pages -│ └── common/ # Shared pages (auth, etc.) +│ │ ├── config/ # Config pages (Stages, Users, etc.) +│ │ └── analytics/ # Analytics pages (OEE, QRM, etc.) +│ ├── auth/ # Auth pages (Auth, AcceptInvitation) +│ ├── operator/ # Operator pages (WorkQueue, etc.) +│ └── common/ # Shared pages (MyPlan, Pricing, Help, etc.) ├── hooks/ # Custom React hooks ├── lib/ # Utility libraries ├── integrations/ # Supabase client ├── i18n/ # Localization -│ └── locales/ # Translation files +│ └── locales/ # Translation files (en, nl, de) ├── styles/ # Global styles │ └── design-system.css +├── routes.ts # Centralized route definitions └── theme/ # Theme provider ``` @@ -549,20 +553,67 @@ function JobCard({ job, onEdit, onDelete }: JobCardProps) { |------|---------| | `docs/DESIGN_SYSTEM.md` | Design guidelines (READ FIRST for UI work) | | `src/styles/design-system.css` | CSS tokens and classes | +| `src/routes.ts` | Centralized route definitions | | `src/integrations/supabase/client.ts` | Supabase client | | `src/integrations/supabase/types.ts` | Database types | | `src/i18n/locales/*/translation.json` | Translations (EN/NL/DE) | | `src/components/ui/*` | shadcn/ui components | | `src/theme/ThemeProvider.tsx` | Theme (dark/light/auto) | +### Page Organization + +Pages are organized by role with barrel exports (`index.ts`): + +``` +src/pages/ +├── admin/ +│ ├── config/ # Config pages +│ │ ├── index.ts # Barrel export +│ │ ├── Stages.tsx +│ │ ├── Materials.tsx +│ │ └── Users.tsx # etc. +│ ├── analytics/ # Analytics pages +│ │ ├── index.ts # Barrel export +│ │ └── OEEAnalytics.tsx # etc. +│ └── Dashboard.tsx # Other admin pages +├── auth/ +│ ├── index.ts # Barrel export +│ ├── Auth.tsx +│ └── AcceptInvitation.tsx +├── operator/ +│ ├── index.ts # Barrel export +│ └── WorkQueue.tsx # etc. +└── common/ + ├── index.ts # Barrel export + ├── MyPlan.tsx + ├── Pricing.tsx + └── Help.tsx # etc. +``` + ### Adding New Files Follow existing patterns: -- **Page**: `src/pages/admin/NewFeaturePage.tsx` +- **Admin page**: `src/pages/admin/NewFeature.tsx` +- **Config page**: `src/pages/admin/config/NewConfig.tsx` (add to barrel export) +- **Common page**: `src/pages/common/NewPage.tsx` (add to barrel export) - **Component**: `src/components/admin/NewFeatureComponent.tsx` - **Hook**: `src/hooks/useNewFeature.ts` - **Type**: Add to `src/integrations/supabase/types.ts` +### Import Pattern + +Use barrel exports for cleaner imports: + +```tsx +// Good - use barrel exports +import { Auth, AcceptInvitation } from "./pages/auth"; +import { ApiKeys, Materials, Users } from "./pages/admin/config"; +import { MyPlan, Pricing, Help } from "./pages/common"; + +// Avoid - direct file imports (unless not in barrel) +import Auth from "./pages/auth/Auth"; +``` + --- ## Common Patterns diff --git a/src/App.tsx b/src/App.tsx index a82d0484..9b7c057f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,26 +7,18 @@ import { AuthProvider, useAuth } from "./contexts/AuthContext"; import { ThemeProvider } from "./theme/ThemeProvider"; import { NotificationToastProvider } from "./components/NotificationToastProvider"; import { McpActivityToasts } from "./components/admin/McpActivityToasts"; -import Auth from "./pages/Auth"; -import AcceptInvitation from "./pages/AcceptInvitation"; -import WorkQueue from "./pages/operator/WorkQueue"; -import MyActivity from "./pages/operator/MyActivity"; -import MyIssues from "./pages/operator/MyIssues"; -import OperatorTerminal from "./pages/operator/OperatorTerminal"; +// Auth pages +import { Auth, AcceptInvitation } from "./pages/auth"; + +// Operator pages +import { WorkQueue, MyActivity, MyIssues, OperatorTerminal, OperatorView } from "./pages/operator"; + +// Admin pages import Dashboard from "./pages/admin/Dashboard"; import IssueQueue from "./pages/admin/IssueQueue"; -import ConfigStages from "./pages/admin/ConfigStages"; import FactoryCalendar from "./pages/admin/FactoryCalendar"; -import ConfigMaterials from "./pages/admin/ConfigMaterials"; -import ConfigResources from "./pages/admin/ConfigResources"; -import ConfigUsers from "./pages/admin/ConfigUsers"; -import ConfigScrapReasons from "./pages/admin/ConfigScrapReasons"; import OrganizationSettings from "./pages/admin/OrganizationSettings"; import Assignments from "./pages/admin/Assignments"; -import ConfigApiKeys from "./pages/admin/ConfigApiKeys"; -import ConfigMcpKeys from "./pages/admin/ConfigMcpKeys"; -import ConfigWebhooks from "./pages/admin/ConfigWebhooks"; -import ConfigMqttPublishers from "./pages/admin/ConfigMqttPublishers"; import McpServerSettings from "./pages/admin/McpServerSettings"; import DataExport from "./pages/admin/DataExport"; import DataImport from "./pages/admin/DataImport"; @@ -41,19 +33,43 @@ import IntegrationsMarketplace from "./pages/admin/IntegrationsMarketplace"; import Shipments from "./pages/admin/Shipments"; import StepsTemplatesView from "./pages/admin/StepsTemplatesView"; import AnalyticsDashboard from "./pages/admin/Analytics"; -import OEEAnalytics from "./pages/admin/analytics/OEEAnalytics"; -import ReliabilityAnalytics from "./pages/admin/analytics/ReliabilityAnalytics"; -import QRMAnalytics from "./pages/admin/analytics/QRMAnalytics"; -import QRMDashboard from "./pages/admin/analytics/QRMDashboard"; -import JobsAnalytics from "./pages/admin/analytics/JobsAnalytics"; -import QualityAnalytics from "./pages/admin/analytics/QualityAnalytics"; -import ApiDocs from "./pages/ApiDocs"; -import Pricing from "./pages/Pricing"; -import { MyPlan } from "./pages/MyPlan"; -import Help from "./pages/common/Help"; -import About from "./pages/About"; -import PrivacyPolicy from "./pages/common/PrivacyPolicy"; -import TermsOfService from "./pages/common/TermsOfService"; + +// Admin config pages +import { + ApiKeys as ConfigApiKeys, + Materials as ConfigMaterials, + McpKeys as ConfigMcpKeys, + MqttPublishers as ConfigMqttPublishers, + Resources as ConfigResources, + ScrapReasons as ConfigScrapReasons, + Stages as ConfigStages, + Users as ConfigUsers, + Webhooks as ConfigWebhooks, +} from "./pages/admin/config"; + +// Admin analytics pages +import { + OEEAnalytics, + ReliabilityAnalytics, + QRMAnalytics, + QRMDashboard, + JobsAnalytics, + QualityAnalytics, +} from "./pages/admin/analytics"; + +// Common pages +import { + ApiDocs, + Pricing, + MyPlan, + Help, + About, + PrivacyPolicy, + TermsOfService, + SubscriptionBlocked, +} from "./pages/common"; + +// Other pages import NotFound from "./pages/NotFound"; import { OnboardingWizard } from "./components/onboarding"; import Layout from "./components/Layout"; diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx deleted file mode 100644 index 1d0e3831..00000000 --- a/src/layouts/AdminLayout.tsx +++ /dev/null @@ -1,626 +0,0 @@ -import { useAuth } from "@/contexts/AuthContext"; -import { Button } from "@/components/ui/button"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; -import { Badge } from "@/components/ui/badge"; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { - LogOut, - LayoutDashboard, - ListChecks, - Settings, - AlertCircle, - UserCheck, - BookOpen, - Briefcase, - Package, - Plug, - ChevronLeft, - ChevronRight, - ChevronDown, - Menu, - Store, - Clock, - Layers, - Database, - Wrench, - FileText, - DollarSign, - Users, - Key, - Webhook, - FileDown, - HelpCircle, - CreditCard, - Code, - Eye, - ListTodo, - Activity, - Flag, - Info, - Truck, - Trash2, - Radio, -} from "lucide-react"; -import { Link, useLocation } from "react-router-dom"; -import { useState } from "react"; -import { cn } from "@/lib/utils"; -import { usePendingIssuesCount } from "@/hooks/usePendingIssuesCount"; -import { LanguageSwitcher } from "@/components/LanguageSwitcher"; -import { ROUTES } from "@/routes"; -import { useTranslation } from "react-i18next"; -import AnimatedBackground from "@/components/AnimatedBackground"; -import { BrandLogo } from "@/components/ui/brand-logo"; -import SessionTrackingBar from "@/components/SessionTrackingBar"; - -interface AdminLayoutProps { - children: React.ReactNode; -} - -export default function AdminLayout({ children }: AdminLayoutProps) { - const { t } = useTranslation(); - const { profile, tenant, signOut } = useAuth(); - const location = useLocation(); - const [collapsed, setCollapsed] = useState(false); - const [mobileOpen, setMobileOpen] = useState(false); - const [operatorViewsOpen, setOperatorViewsOpen] = useState(false); - const [configOpen, setConfigOpen] = useState(false); - const [integrationsOpen, setIntegrationsOpen] = useState(false); - const [accountOpen, setAccountOpen] = useState(false); - const { count: pendingIssuesCount } = usePendingIssuesCount(); - - const isActive = (path: string) => location.pathname === path; - const isActiveGroup = (...paths: string[]) => paths.some(path => location.pathname.startsWith(path)); - - // Main navigation - Daily operations (always visible) - const mainNavItems = [ - { - path: ROUTES.ADMIN.DASHBOARD, - label: t("navigation.dashboard"), - icon: LayoutDashboard, - exact: true, - }, - { - path: ROUTES.ADMIN.JOBS, - label: t("navigation.jobs"), - icon: Briefcase, - activePaths: [ROUTES.ADMIN.JOBS], - }, - { - path: ROUTES.ADMIN.PARTS, - label: t("navigation.parts"), - icon: Package, - exact: true, - }, - { - path: ROUTES.ADMIN.OPERATIONS, - label: t("navigation.operations"), - icon: Layers, - exact: true, - }, - { - path: ROUTES.ADMIN.ASSIGNMENTS, - label: t("navigation.assignments"), - icon: UserCheck, - exact: true, - }, - { - path: ROUTES.ADMIN.ISSUES, - label: t("navigation.issues"), - icon: AlertCircle, - exact: true, - badge: pendingIssuesCount, - }, - { - path: ROUTES.ADMIN.ACTIVITY, - label: t("navigation.activityMonitor"), - icon: Clock, - exact: true, - }, - { - path: ROUTES.ADMIN.SHIPPING, - label: t("navigation.shipping"), - icon: Truck, - exact: true, - }, - ]; - - // Operator views - Admin can see what operators see - const operatorViewItems = [ - { - path: ROUTES.OPERATOR.WORK_QUEUE, - label: t("navigation.workQueue"), - icon: ListTodo, - exact: true, - }, - { - path: ROUTES.OPERATOR.VIEW, - label: t("navigation.operatorView"), - icon: Eye, - exact: true, - }, - { - path: ROUTES.OPERATOR.MY_ACTIVITY, - label: t("navigation.myActivity"), - icon: Activity, - exact: true, - }, - { - path: ROUTES.OPERATOR.MY_ISSUES, - label: t("navigation.myIssues"), - icon: Flag, - exact: true, - }, - ]; - - // Configuration - Setup tasks (rarely changed) - const configNavItems = [ - { - path: ROUTES.ADMIN.CONFIG.STAGES, - label: t("navigation.stages"), - icon: Database, - exact: true, - }, - { - path: ROUTES.ADMIN.CONFIG.MATERIALS, - label: t("navigation.materials"), - icon: Wrench, - exact: true, - }, - { - path: ROUTES.ADMIN.CONFIG.RESOURCES, - label: t("navigation.resources"), - icon: Wrench, - exact: true, - }, - { - path: ROUTES.ADMIN.CONFIG.USERS, - label: t("navigation.users"), - icon: Users, - exact: true, - }, - { - path: ROUTES.ADMIN.CONFIG.STEPS_TEMPLATES, - label: t("navigation.templates"), - icon: FileText, - exact: true, - }, - { - path: ROUTES.ADMIN.CONFIG.SCRAP_REASONS, - label: t("navigation.scrapReasons", "Scrap Reasons"), - icon: Trash2, - exact: true, - }, - { - path: ROUTES.ADMIN.CONFIG.API_KEYS, - label: t("navigation.apiKeys"), - icon: Key, - exact: true, - }, - { - path: ROUTES.ADMIN.CONFIG.WEBHOOKS, - label: t("navigation.webhooks"), - icon: Webhook, - exact: true, - }, - ]; - - // Integrations - Developer tools - const integrationsNavItems = [ - { - path: ROUTES.ADMIN.INTEGRATIONS, - label: t("navigation.integrations"), - icon: Store, - exact: true, - }, - { - path: ROUTES.ADMIN.CONFIG.MQTT_PUBLISHERS, - label: t("navigation.mqttPublishers"), - icon: Radio, - exact: true, - }, - { - path: ROUTES.ADMIN.DATA_EXPORT, - label: t("navigation.dataExport"), - icon: FileDown, - exact: true, - }, - { - path: ROUTES.COMMON.API_DOCS, - label: t("apiDocumentation"), - icon: FileText, - exact: true, - }, - ]; - - // Account & Support - const accountNavItems = [ - { - path: ROUTES.COMMON.MY_PLAN, - label: t("myPlan"), - icon: CreditCard, - exact: true, - }, - { - path: ROUTES.COMMON.PRICING, - label: t("navigation.pricing"), - icon: DollarSign, - exact: true, - }, - { - path: ROUTES.ADMIN.SETTINGS, - label: t("navigation.settings"), - icon: Settings, - exact: true, - }, - { - path: ROUTES.COMMON.HELP, - label: t("help"), - icon: HelpCircle, - exact: true, - }, - { - path: ROUTES.COMMON.ABOUT, - label: t("about"), - icon: Info, - exact: true, - }, - ]; - - const SidebarContent = () => ( -
- {/* Logo/Brand */} -
- -
- - {/* Navigation */} - - {/* Main Navigation - Daily Operations */} -
- {mainNavItems.map((item) => { - const isItemActive = item.exact - ? isActive(item.path) - : item.activePaths - ? isActiveGroup(...item.activePaths) - : location.pathname.startsWith(item.path); - - return ( - setMobileOpen(false)}> - - - ); - })} -
- - {!collapsed && } - - {/* Operator Views Section - Collapsible */} - {!collapsed && ( - - - - - - {operatorViewItems.map((item) => { - const isItemActive = item.exact - ? isActive(item.path) - : location.pathname.startsWith(item.path); - - return ( - setMobileOpen(false)}> - - - ); - })} - - - )} - - {!collapsed && } - - {/* Configuration Section - Collapsible */} - {!collapsed && ( - - - - - - {configNavItems.map((item) => { - const isItemActive = item.exact - ? isActive(item.path) - : location.pathname.startsWith(item.path); - - return ( - setMobileOpen(false)}> - - - ); - })} - - - )} - - {!collapsed && } - - {/* Integrations Section - Collapsible */} - {!collapsed && ( - - - - - - {integrationsNavItems.map((item) => { - const isItemActive = item.exact - ? isActive(item.path) - : location.pathname.startsWith(item.path); - - return ( - setMobileOpen(false)}> - - - ); - })} - - - )} - - {!collapsed && } - - {/* Account & Support Section - Collapsible */} - {!collapsed && ( - - - - - - {accountNavItems.map((item) => { - const isItemActive = item.exact - ? isActive(item.path) - : location.pathname.startsWith(item.path); - - return ( - setMobileOpen(false)}> - - - ); - })} - - - )} -
- - {/* User Profile & Sign Out */} -
- {!collapsed && tenant && ( -
-
Tenant
-
{tenant.company_name || tenant.name}
-
- - {tenant.plan} - - - {tenant.status} - -
-
- )} - {!collapsed && profile && ( -
-
{profile.full_name}
-
- {profile.role} -
-
- )} -
- -
- -
- - {/* Collapse Toggle (Desktop) */} - {!mobileOpen && ( - - )} -
- ); - - return ( - <> - -
- {/* Mobile Menu Button */} - - - {/* Mobile Sidebar Overlay */} - {mobileOpen && ( -
setMobileOpen(false)} - /> - )} - - {/* Sidebar - Mobile */} - - - {/* Sidebar - Desktop */} - - - {/* Main Content */} -
-
- {children} -
-
- - {/* Global Session Tracking Bar */} - -
- - ); -} diff --git a/src/layouts/Layout.tsx b/src/layouts/Layout.tsx deleted file mode 100644 index 55645d9b..00000000 --- a/src/layouts/Layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Role-based layout routing -import { useAuth } from "@/contexts/AuthContext"; -import AdminLayout from "@/layouts/AdminLayout"; -import { OperatorLayout } from "@/layouts/OperatorLayout"; - -interface LayoutProps { - children: React.ReactNode; -} - -// SECURITY NOTE: Layout selection based on role is for UI convenience only. -// Server-side RLS policies enforce actual data access permissions. -export default function Layout({ children }: LayoutProps) { - const { profile, loading } = useAuth(); - - // Show nothing while loading to prevent layout flicker - if (loading || !profile) { - return null; - } - - // UI-only: Route to appropriate layout based on user role - // This provides better UX but provides ZERO security - if (profile.role === "admin") { - return {children}; - } - - // Default to operator layout for operator role - return {children}; -} diff --git a/src/layouts/OperatorLayout.tsx b/src/layouts/OperatorLayout.tsx deleted file mode 100644 index 6774cc03..00000000 --- a/src/layouts/OperatorLayout.tsx +++ /dev/null @@ -1,265 +0,0 @@ -"use client"; - -import * as React from "react"; -import { useTranslation } from "react-i18next"; -import { useLocation, useNavigate } from "react-router-dom"; -import { - List, - Clock, - AlertTriangle, - LogOut, - Sun, - Moon, - HelpCircle, - Building2, - Gauge, - ChevronDown, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { cn } from "@/lib/utils"; -import { useAuth } from "@/contexts/AuthContext"; -import { useThemeMode } from "@/theme/ThemeProvider"; -import CurrentlyTimingWidget from "@/components/operator/CurrentlyTimingWidget"; -import { LanguageSwitcher } from "@/components/LanguageSwitcher"; -import { AppTour } from "@/components/onboarding"; -import { ROUTES } from "@/routes"; -import { BrandLogo } from "@/components/ui/brand-logo"; -import SessionTrackingBar from "@/components/SessionTrackingBar"; - -interface OperatorLayoutProps { - children: React.ReactNode; -} - -export const OperatorLayout: React.FC = ({ children }) => { - const { t } = useTranslation(); - const { profile, tenant, signOut } = useAuth(); - const { mode, toggleTheme } = useThemeMode(); - const location = useLocation(); - const navigate = useNavigate(); - - const navItems = [ - { path: ROUTES.OPERATOR.WORK_QUEUE, label: t("navigation.workQueue"), icon: List }, - { path: ROUTES.OPERATOR.VIEW, label: t("navigation.operatorView"), icon: Gauge }, - { path: ROUTES.OPERATOR.MY_ACTIVITY, label: t("navigation.myActivity"), icon: Clock }, - { path: ROUTES.OPERATOR.MY_ISSUES, label: t("navigation.myIssues"), icon: AlertTriangle }, - ]; - - const isActive = (path: string) => location.pathname === path; - - return ( -
- {/* Top App Bar - Glassmorphism Header */} -
-
- {/* Logo/Brand */} - - - {/* Right Side Actions */} -
- {/* Language Switcher */} - - - {/* Theme Toggle */} - - - {/* User Avatar and Menu */} - - - - - - {/* Company/Tenant Section */} - {tenant && ( - <> -
-
- - - {tenant.company_name || tenant.name} - -
-
- - )} - - -
- {profile?.full_name} - - {profile?.email} - - - {t("app.operator")} - -
-
- - navigate(ROUTES.COMMON.HELP)} - className="cursor-pointer focus:bg-white/10" - > - - {t("docsAndHelp")} - - - - - {t("auth.signOut")} - -
-
-
-
-
- - {/* Desktop Navigation Tabs - Hidden on mobile */} -
-
- {navItems.map((item) => { - const Icon = item.icon; - return ( - - ); - })} -
-
- - {/* Currently Timing Widget - Sticky below header */} -
-
- -
-
- - {/* Main Content */} -
- {children} -
- - {/* Bottom Navigation - Mobile Only */} - - - {/* Global Session Tracking Bar */} - - - {/* Onboarding Tour - only show if not completed */} - {profile && !(profile as any).tour_completed && } -
- ); -}; diff --git a/src/pages/About.tsx b/src/pages/About.tsx deleted file mode 100644 index f4dea377..00000000 --- a/src/pages/About.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Card, CardContent } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; - -export default function About() { - return ( -
-

About

- - - -

Eryxon MES

-

- Manufacturing execution system for metals fabrication. Track jobs, parts, and operations through production. -

-

- Made by{" "} - - Sheet Metal Connect - -

-
-
- - - -
-
- - Help - - {" · "} - - API Docs - - {" · "} - - GitHub - -
-
- Privacy Policy · Terms of Service -
-
-
- ); -} diff --git a/src/pages/ApiDocs.tsx b/src/pages/ApiDocs.tsx deleted file mode 100644 index bb75ca3b..00000000 --- a/src/pages/ApiDocs.tsx +++ /dev/null @@ -1,856 +0,0 @@ -import { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; -import SwaggerUI from "swagger-ui-react"; -import "swagger-ui-react/swagger-ui.css"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Separator } from "@/components/ui/separator"; -import { Badge } from "@/components/ui/badge"; -import { - BookOpen, - Code2, - KeyRound, - Rocket, - CheckCircle2, - Copy, - ExternalLink, - Terminal, - Zap, - FileJson, - PlayCircle, - Download, - FileCode, - Link as LinkIcon, - Package, - FileUp, - HelpCircle -} from "lucide-react"; -import { useToast } from "@/hooks/use-toast"; - -export default function ApiDocs() { - const { toast } = useToast(); - const [apiKey, setApiKey] = useState(""); - const baseUrl = import.meta.env.VITE_SUPABASE_URL?.replace('/supabase', '') || "https://vatgianzotsurljznsry.supabase.co"; - const apiBaseUrl = `${baseUrl}/functions/v1`; - - const copyToClipboard = (text: string, label: string) => { - navigator.clipboard.writeText(text); - toast({ - title: "Copied!", - description: `${label} copied to clipboard`, - }); - }; - - const downloadSpec = async (format: 'json' | 'yaml') => { - try { - const response = await fetch('/openapi.json'); - const spec = await response.json(); - - let content: string; - let filename: string; - let mimeType: string; - - if (format === 'json') { - content = JSON.stringify(spec, null, 2); - filename = 'eryxon-flow-openapi.json'; - mimeType = 'application/json'; - } else { - // Convert JSON to YAML (simple conversion) - content = jsonToYaml(spec); - filename = 'eryxon-flow-openapi.yaml'; - mimeType = 'application/x-yaml'; - } - - const blob = new Blob([content], { type: mimeType }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - toast({ - title: "Downloaded!", - description: `OpenAPI spec downloaded as ${filename}`, - }); - } catch (error) { - toast({ - title: "Error", - description: "Failed to download spec", - variant: "destructive", - }); - } - }; - - // Simple JSON to YAML converter (handles common cases) - const jsonToYaml = (obj: any, indent = 0): string => { - const spaces = ' '.repeat(indent); - let result = ''; - - if (Array.isArray(obj)) { - for (const item of obj) { - if (typeof item === 'object' && item !== null) { - result += `${spaces}-\n${jsonToYaml(item, indent + 1).replace(/^ /, '')}`; - } else { - result += `${spaces}- ${formatYamlValue(item)}\n`; - } - } - } else if (typeof obj === 'object' && obj !== null) { - for (const [key, value] of Object.entries(obj)) { - if (typeof value === 'object' && value !== null) { - if (Array.isArray(value) && value.length === 0) { - result += `${spaces}${key}: []\n`; - } else if (typeof value === 'object' && Object.keys(value).length === 0) { - result += `${spaces}${key}: {}\n`; - } else { - result += `${spaces}${key}:\n${jsonToYaml(value, indent + 1)}`; - } - } else { - result += `${spaces}${key}: ${formatYamlValue(value)}\n`; - } - } - } - - return result; - }; - - const formatYamlValue = (value: any): string => { - if (value === null) return 'null'; - if (value === undefined) return ''; - if (typeof value === 'boolean') return value ? 'true' : 'false'; - if (typeof value === 'number') return String(value); - if (typeof value === 'string') { - if (value.includes('\n') || value.includes(':') || value.includes('#') || - value.includes('"') || value.includes("'") || value.startsWith(' ') || - value.endsWith(' ') || /^[\d.]+$/.test(value) || value === '') { - return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`; - } - return value; - } - return String(value); - }; - - const openInSwaggerEditor = () => { - const specUrl = encodeURIComponent(`${window.location.origin}/openapi.json`); - window.open(`https://editor.swagger.io/?url=${specUrl}`, '_blank'); - }; - - const codeExamples = { - curl: `# Test your API connection -curl ${apiBaseUrl}/api-stages \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -H "Content-Type: application/json" - -# Create a new job -curl -X POST ${apiBaseUrl}/api-jobs \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -H "Content-Type: application/json" \\ - -d '{ - "job_number": "JOB-2024-001", - "customer": "Acme Corp", - "due_date": "2024-12-31", - "parts": [{ - "part_number": "BRACKET-001", - "material": "Steel 304", - "quantity": 10 - }] - }'`, - - javascript: `// Using fetch API -const apiKey = 'YOUR_API_KEY'; -const baseUrl = '${apiBaseUrl}'; - -// Fetch stages -async function getStages() { - const response = await fetch(\`\${baseUrl}/api-stages\`, { - headers: { - 'Authorization': \`Bearer \${apiKey}\`, - 'Content-Type': 'application/json' - } - }); - - const data = await response.json(); - return data; -} - -// Create a job -async function createJob(jobData) { - const response = await fetch(\`\${baseUrl}/api-jobs\`, { - method: 'POST', - headers: { - 'Authorization': \`Bearer \${apiKey}\`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(jobData) - }); - - const data = await response.json(); - return data; -} - -// Example usage -const newJob = { - job_number: 'JOB-2024-001', - customer: 'Acme Corp', - due_date: '2024-12-31', - parts: [{ - part_number: 'BRACKET-001', - material: 'Steel 304', - quantity: 10 - }] -}; - -createJob(newJob).then(console.log);`, - - python: `import requests -import json - -API_KEY = 'YOUR_API_KEY' -BASE_URL = '${apiBaseUrl}' - -headers = { - 'Authorization': f'Bearer {API_KEY}', - 'Content-Type': 'application/json' -} - -# Test connection -response = requests.get(f'{BASE_URL}/api-stages', headers=headers) -print(response.json()) - -# Create a job -job_data = { - 'job_number': 'JOB-2024-001', - 'customer': 'Acme Corp', - 'due_date': '2024-12-31', - 'parts': [{ - 'part_number': 'BRACKET-001', - 'material': 'Steel 304', - 'quantity': 10 - }] -} - -response = requests.post( - f'{BASE_URL}/api-jobs', - headers=headers, - json=job_data -) - -print(response.json())`, - - nodejs: `// Using axios (npm install axios) -const axios = require('axios'); - -const apiClient = axios.create({ - baseURL: '${apiBaseUrl}', - headers: { - 'Authorization': 'Bearer YOUR_API_KEY', - 'Content-Type': 'application/json' - } -}); - -// Get stages -apiClient.get('/api-stages') - .then(response => console.log(response.data)) - .catch(error => console.error(error)); - -// Create a job -const jobData = { - job_number: 'JOB-2024-001', - customer: 'Acme Corp', - due_date: '2024-12-31', - parts: [{ - part_number: 'BRACKET-001', - material: 'Steel 304', - quantity: 10 - }] -}; - -apiClient.post('/api-jobs', jobData) - .then(response => console.log(response.data)) - .catch(error => console.error(error));` - }; - - // Custom Swagger UI plugin to inject API key - const ApiKeyPlugin = () => { - return { - wrapComponents: { - authorizeBtn: (Original: any) => (props: any) => { - return null; // Hide default authorize button - } - } - }; - }; - - return ( - <> -
- {/* Hero Section */} - - -
-
- - - Eryxon Flow API - - - Production workflow management API for sheet metal manufacturing. - Build integrations, automate workflows, and manage your production data programmatically. - -
- - v1.0.0 - -
-
- -
-
- -
-
REST API
-
JSON-based RESTful API
-
-
-
- -
-
API Key Auth
-
Secure bearer token authentication
-
-
-
- -
-
Interactive Docs
-
Test endpoints directly in browser
-
-
-
-
-
- - {/* Download & Tools Section */} - - - - - OpenAPI Specification - - - Download the spec to use with Swagger, Postman, or generate client SDKs - - - -
- {/* Download JSON */} - - - {/* Download YAML */} - - - {/* Open in Swagger Editor */} - - - {/* Copy Spec URL */} - -
- - {/* Postman Import Instructions */} -
-
- -
-
Import to Postman
-
- Open Postman → Import → Link → Paste: {window.location.origin}/openapi.json -
- -
-
-
- - {/* Related Resources */} -
- - - - - - -
-
-
- - - - - - Quick Start - - - - Code Examples - - - - Try It Out - - - - Full Reference - - - - {/* Quick Start Tab */} - - - - - - Getting Started in 3 Steps - - - Start making API calls in minutes with this beginner-friendly guide - - - - {/* Step 1 */} -
-
-
- 1 -
-

Generate an API Key

-
-
-

- Navigate to Admin > API Keys in the web interface and generate a new API key. - Your key will start with ery_live_ or ery_test_ -

- - - Keep your API key secure! - - Your API key is shown only once. Store it securely and never commit it to version control. - - - -
-
- - - - {/* Step 2 */} -
-
-
- 2 -
-

Test Your Connection

-
-
-

- Make your first API call to verify your key works. We recommend starting with the read-only /api-stages endpoint. -

-
-
- Base URL: - -
-
- {apiBaseUrl} -
-
-
-
- cURL Example: - -
-
-                        {`curl ${apiBaseUrl}/api-stages \\
-  -H "Authorization: Bearer YOUR_API_KEY"`}
-                      
-
- - - Expected Response - -
-                          {`{
-  "success": true,
-  "data": {
-    "stages": [...]
-  }
-}`}
-                        
-
-
-
-
- - - - {/* Step 3 */} -
-
-
- 3 -
-

Start Building

-
-
-

- Explore the API Reference below to see all available endpoints and try them interactively. -

-
- -
Common Use Cases
-
    -
  • Create jobs from ERP systems
  • -
  • Track production progress
  • -
  • Update task completion
  • -
  • Query job status and metrics
  • -
-
- -
Best Practices
-
    -
  • Use pagination for large datasets
  • -
  • Implement exponential backoff
  • -
  • Handle rate limit headers
  • -
  • Validate responses
  • -
-
-
-
-
-
-
-
- - {/* Code Examples Tab */} - - - - - - Code Examples - - - Ready-to-use code snippets in multiple programming languages - - - - - - cURL - JavaScript - Node.js - Python - - - {Object.entries(codeExamples).map(([lang, code]) => ( - -
- -
-                          {code}
-                        
-
-
- ))} -
-
-
- - {/* Authentication Card */} - - - - - Authentication - - - -

- All API requests require authentication using an API key in the Authorization header: -

-
-
- Header Format: - -
-
-                    Authorization: Bearer ery_live_your_api_key_here
-                  
-
- - - Rate Limiting - - API requests are rate-limited. Check the X-RateLimit-* headers in responses - for current limits and usage. - - -
-
-
- - {/* Try It Out Tab - Interactive Testing */} - - - - - - Interactive API Testing - - - Test API endpoints directly in your browser. No additional tools required! - - - - - - How to Test - -
    -
  1. Click the "Authorize" button below (green lock icon)
  2. -
  3. Enter your API key: ery_live_xxxxx
  4. -
  5. Expand any endpoint and click "Try it out"
  6. -
  7. Fill in parameters and click "Execute"
  8. -
-
-
- - {/* Quick Test Endpoints */} -
- -
- GET - /api-stages -
-

Best endpoint to test your API key. Returns all production stages.

-
- -
- GET - /api-jobs -
-

List all jobs with filtering. Test pagination with limit/offset.

-
- -
- POST - /api-jobs -
-

Create a new job. Includes example request body.

-
- -
- PUT - /api-jobs/sync -
-

ERP sync endpoint. Upsert by external_id.

-
-
- - - -
- -
-
-
-
- - {/* API Reference Tab - Full Spec */} - - - - - - Complete API Reference - - - Full OpenAPI specification with all endpoints, schemas, and examples. - - - -
- - - -
-
- -
-
-
-
-
-
- - - - ); -} diff --git a/src/pages/MyPlan.tsx b/src/pages/MyPlan.tsx deleted file mode 100644 index 59f721dd..00000000 --- a/src/pages/MyPlan.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import { Card, CardContent } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { - Info, - Mail, - Clock, - Loader2, -} from 'lucide-react'; -import { useSubscription } from '../hooks/useSubscription'; -import { useTranslation } from 'react-i18next'; - -export const MyPlan: React.FC = () => { - const { t } = useTranslation(); - const { loading, error } = useSubscription(); - - const handleContactUs = () => { - window.location.href = `mailto:office@sheetmetalconnect.com?subject=${encodeURIComponent('Subscription Inquiry')}`; - }; - - if (loading) { - return ( -
- -
- ); - } - - if (error) { - return ( - - {error} - - ); - } - - return ( -
- {/* Header */} -
-

{t('myPlan.title')}

-

{t('myPlan.comingSoonSubtitle')}

-
- - {/* Coming Soon */} - - - -

{t('myPlan.comingSoonTitle')}

-

- {t('myPlan.comingSoonDescription')} -

- -
-
- - {/* Info Alert */} - - - -
{t('myPlan.currentlyFree')}
-
- {t('myPlan.currentlyFreeDescription')} -
-
-
-
- ); -}; diff --git a/src/pages/Pricing.tsx b/src/pages/Pricing.tsx deleted file mode 100644 index f05d1f14..00000000 --- a/src/pages/Pricing.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Check, X, Mail, Users, Zap, Star, ArrowRight } from "lucide-react"; -import { useSubscription } from "@/hooks/useSubscription"; -import { Link } from "react-router-dom"; -import { useTranslation } from "react-i18next"; - -// Main hosted tiers -const hostedTiers = [ - { - id: "free", - nameKey: "pricing.free.name", - descriptionKey: "pricing.free.description", - priceKey: "pricing.free.price", - featuresKey: "pricing.free.features", - ctaKey: "pricing.currentPlan", - icon: Users, - popular: false, - gradient: false, - }, - { - id: "pro", - nameKey: "pricing.pro.name", - descriptionKey: "pricing.pro.description", - priceKey: "pricing.pro.price", - featuresKey: "pricing.pro.features", - ctaKey: "pricing.upgrade", - icon: Zap, - popular: true, - gradient: false, - }, - { - id: "premium", - nameKey: "pricing.premium.name", - descriptionKey: "pricing.premium.description", - priceKey: "pricing.premium.price", - featuresKey: "pricing.premium.features", - ctaKey: "pricing.contactSales", - icon: Star, - popular: false, - gradient: true, - }, -]; - - -export default function Pricing() { - const { t } = useTranslation(); - const { subscription, getPlanDisplayName } = useSubscription(); - const currentPlan = subscription?.plan || 'free'; - - const handleUpgradeRequest = (tierName: string) => { - const subject = `Upgrade Request: ${tierName} Plan`; - const body = `Hello, - -I would like to request an upgrade to the ${tierName} plan. - -Current Plan: ${getPlanDisplayName(currentPlan)} -Tenant ID: ${subscription?.tenant_id || 'N/A'} - -Please provide me with more information about the upgrade process. - -Thank you!`; - - window.location.href = `mailto:office@sheetmetalconnect.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; - }; - - const isCurrentPlan = (tierId: string) => tierId === currentPlan; - - return ( -
- {/* Header */} -
-

{t("pricing.title")}

-

- {t("pricing.subtitle")} -

- {subscription && ( -
- - {t("pricing.currentPlan")}: {getPlanDisplayName(currentPlan)} - - - - -
- )} -
- - {/* Section 1: Hosted Plans */} -
-
-

{t("pricing.hostedPlans")}

-

- {t("pricing.hostedPlansDescription")} -

-
- -
- {hostedTiers.map((tier) => { - const Icon = tier.icon; - const isCurrent = isCurrentPlan(tier.id); - - return ( - - {isCurrent && ( - - {t("pricing.currentPlan")} - - )} - {tier.popular && !isCurrent && ( - - {t("onboarding.mostPopular")} - - )} - - -
-
- -
-
- {t(tier.nameKey)} - {t(tier.descriptionKey)} -
-
- -
-
- {t(tier.priceKey)} - {t("pricing.perMonth")} -
-
-
- - -
    - {(t(tier.featuresKey, { returnObjects: true }) as string[]).map((feature, index) => { - const isNegative = feature.toLowerCase().startsWith('no ') || - feature.toLowerCase().startsWith('geen ') || - feature.toLowerCase().startsWith('kein'); - return ( -
  • - {isNegative ? ( - - ) : ( - - )} - {feature} -
  • - ); - })} -
-
- - - {isCurrent ? ( - - - - ) : ( - - )} - -
- ); - })} -
-
- -
- ); -} diff --git a/src/pages/admin/analytics/index.ts b/src/pages/admin/analytics/index.ts new file mode 100644 index 00000000..187f7d45 --- /dev/null +++ b/src/pages/admin/analytics/index.ts @@ -0,0 +1,7 @@ +// Analytics pages barrel export +export { default as JobsAnalytics } from './JobsAnalytics'; +export { default as OEEAnalytics } from './OEEAnalytics'; +export { default as QRMAnalytics } from './QRMAnalytics'; +export { default as QRMDashboard } from './QRMDashboard'; +export { default as QualityAnalytics } from './QualityAnalytics'; +export { default as ReliabilityAnalytics } from './ReliabilityAnalytics'; diff --git a/src/pages/admin/ConfigApiKeys.tsx b/src/pages/admin/config/ApiKeys.tsx similarity index 100% rename from src/pages/admin/ConfigApiKeys.tsx rename to src/pages/admin/config/ApiKeys.tsx diff --git a/src/pages/admin/ConfigMaterials.tsx b/src/pages/admin/config/Materials.tsx similarity index 100% rename from src/pages/admin/ConfigMaterials.tsx rename to src/pages/admin/config/Materials.tsx diff --git a/src/pages/admin/ConfigMcpKeys.tsx b/src/pages/admin/config/McpKeys.tsx similarity index 100% rename from src/pages/admin/ConfigMcpKeys.tsx rename to src/pages/admin/config/McpKeys.tsx diff --git a/src/pages/admin/ConfigMqttPublishers.tsx b/src/pages/admin/config/MqttPublishers.tsx similarity index 100% rename from src/pages/admin/ConfigMqttPublishers.tsx rename to src/pages/admin/config/MqttPublishers.tsx diff --git a/src/pages/admin/ConfigResources.tsx b/src/pages/admin/config/Resources.tsx similarity index 100% rename from src/pages/admin/ConfigResources.tsx rename to src/pages/admin/config/Resources.tsx diff --git a/src/pages/admin/ConfigScrapReasons.tsx b/src/pages/admin/config/ScrapReasons.tsx similarity index 100% rename from src/pages/admin/ConfigScrapReasons.tsx rename to src/pages/admin/config/ScrapReasons.tsx diff --git a/src/pages/admin/ConfigStages.tsx b/src/pages/admin/config/Stages.tsx similarity index 100% rename from src/pages/admin/ConfigStages.tsx rename to src/pages/admin/config/Stages.tsx diff --git a/src/pages/admin/ConfigUsers.tsx b/src/pages/admin/config/Users.tsx similarity index 100% rename from src/pages/admin/ConfigUsers.tsx rename to src/pages/admin/config/Users.tsx diff --git a/src/pages/admin/ConfigWebhooks.tsx b/src/pages/admin/config/Webhooks.tsx similarity index 100% rename from src/pages/admin/ConfigWebhooks.tsx rename to src/pages/admin/config/Webhooks.tsx diff --git a/src/pages/admin/config/index.ts b/src/pages/admin/config/index.ts new file mode 100644 index 00000000..0f965dc2 --- /dev/null +++ b/src/pages/admin/config/index.ts @@ -0,0 +1,10 @@ +// Config pages barrel export +export { default as ApiKeys } from './ApiKeys'; +export { default as Materials } from './Materials'; +export { default as McpKeys } from './McpKeys'; +export { default as MqttPublishers } from './MqttPublishers'; +export { default as Resources } from './Resources'; +export { default as ScrapReasons } from './ScrapReasons'; +export { default as Stages } from './Stages'; +export { default as Users } from './Users'; +export { default as Webhooks } from './Webhooks'; diff --git a/src/pages/AcceptInvitation.tsx b/src/pages/auth/AcceptInvitation.tsx similarity index 100% rename from src/pages/AcceptInvitation.tsx rename to src/pages/auth/AcceptInvitation.tsx diff --git a/src/pages/Auth.tsx b/src/pages/auth/Auth.tsx similarity index 100% rename from src/pages/Auth.tsx rename to src/pages/auth/Auth.tsx diff --git a/src/pages/auth/index.ts b/src/pages/auth/index.ts new file mode 100644 index 00000000..424e6d35 --- /dev/null +++ b/src/pages/auth/index.ts @@ -0,0 +1,3 @@ +// Auth pages barrel export +export { default as Auth } from './Auth'; +export { default as AcceptInvitation } from './AcceptInvitation'; diff --git a/src/pages/SubscriptionBlocked.tsx b/src/pages/common/SubscriptionBlocked.tsx similarity index 100% rename from src/pages/SubscriptionBlocked.tsx rename to src/pages/common/SubscriptionBlocked.tsx diff --git a/src/pages/common/index.ts b/src/pages/common/index.ts new file mode 100644 index 00000000..15bfc818 --- /dev/null +++ b/src/pages/common/index.ts @@ -0,0 +1,9 @@ +// Common pages barrel export +export { default as About } from './About'; +export { default as ApiDocs } from './ApiDocs'; +export { default as Help } from './Help'; +export { MyPlan } from './MyPlan'; +export { default as Pricing } from './Pricing'; +export { default as PrivacyPolicy } from './PrivacyPolicy'; +export { default as SubscriptionBlocked } from './SubscriptionBlocked'; +export { default as TermsOfService } from './TermsOfService'; diff --git a/src/pages/operator/index.ts b/src/pages/operator/index.ts new file mode 100644 index 00000000..63483cd5 --- /dev/null +++ b/src/pages/operator/index.ts @@ -0,0 +1,6 @@ +// Operator pages barrel export +export { default as MyActivity } from './MyActivity'; +export { default as MyIssues } from './MyIssues'; +export { default as OperatorTerminal } from './OperatorTerminal'; +export { default as OperatorView } from './OperatorView'; +export { default as WorkQueue } from './WorkQueue'; diff --git a/src/routes.ts b/src/routes.ts index 4e36a87e..ea5a6cb0 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -50,28 +50,12 @@ export const ROUTES = { }, COMMON: { - API_DOCS: "/api-docs", - PRICING: "/pricing", - MY_PLAN: "/my-plan", - BILLING_COMING_SOON: "/billing/coming-soon", - HELP: "/help", - ABOUT: "/about", + API_DOCS: "/admin/api-docs", + PRICING: "/admin/pricing", + MY_PLAN: "/admin/my-plan", + HELP: "/admin/help", + ABOUT: "/admin/about", + PRIVACY_POLICY: "/privacy-policy", + TERMS_OF_SERVICE: "/terms-of-service", }, - - // Legacy redirects - LEGACY: { - WORK_QUEUE: "/work-queue", - MY_ACTIVITY: "/my-activity", - MY_ISSUES: "/my-issues", - OPERATOR_VIEW: "/operator-view", - DASHBOARD: "/dashboard", - STAGES: "/admin/stages", - MATERIALS: "/admin/materials", - RESOURCES: "/admin/resources", - USERS: "/admin/users", - API_DOCS: "/api-docs", - PRICING: "/pricing", - MY_PLAN: "/my-plan", - HELP: "/help", - } } as const;