CoolFace
Apppublic

yeswanth0212/token_system

sourceHugging Faceupdated 3mo agoView on Hugging Face
1likes
app.jsx667 linesDownload Raw Back to frontend
1const { useState, useEffect, useRef } = React;2 3const API_URL = '/api';4 5function App() {6    const [counters, setCounters] = useState([]);7    const [waitingTokens, setWaitingTokens] = useState([]);8    const [patientName, setPatientName] = useState('');9    const [selectedDept, setSelectedDept] = useState('General');10    const [isPriority, setIsPriority] = useState(false);11    12    // Navigation & Filters13    const [activeTab, setActiveTab] = useState('dashboard'); // 'dashboard', 'staff', 'analytics'14    const [queueFilter, setQueueFilter] = useState('All');15    16    // Analytics Metrics17    const [analytics, setAnalytics] = useState({18        total_tokens_today: 0,19        total_completed_today: 0,20        total_skipped_today: 0,21        total_cancelled_today: 0,22        avg_wait_time_mins: 0.0,23        avg_serve_time_mins: 0.0,24        by_department: [],25        hourly_throughput: []26    });27 28    // Voice Announcement Ref tracking29    const prevServingRef = useRef({});30    const [speakingCounter, setSpeakingCounter] = useState(null);31 32    // Fetch live queue status33    const fetchQueue = async () => {34        try {35            const res = await fetch(`${API_URL}/queue`);36            if (!res.ok) throw new Error("Network response was not ok");37            const data = await res.json();38            39            // Check if any counter has called a new patient (for Voice Announcement!)40            data.counters.forEach(counter => {41                const prevToken = prevServingRef.current[counter.id];42                const currentToken = counter.current_token;43                44                if (currentToken && (!prevToken || prevToken.id !== currentToken.id)) {45                    // Trigger announcement!46                    announceCall(currentToken.token_number, counter.name, counter.id);47                }48                49                // Update cache50                prevServingRef.current[counter.id] = currentToken;51            });52 53            setCounters(data.counters);54            setWaitingTokens(data.waiting_tokens);55        } catch (error) {56            console.error("Error fetching queue:", error);57        }58    };59 60    // Fetch analytics data61    const fetchAnalytics = async () => {62        try {63            const res = await fetch(`${API_URL}/analytics`);64            if (!res.ok) throw new Error("Network response was not ok");65            const data = await res.json();66            setAnalytics(data);67        } catch (error) {68            console.error("Error fetching analytics:", error);69        }70    };71 72    // Voice Calling Announcer (Web Speech API)73    const announceCall = (tokenNum, counterName, counterId) => {74        if ('speechSynthesis' in window) {75            // Cancel any current utterances to play the newest one instantly76            window.speechSynthesis.cancel();77            78            const message = `Token ${tokenNum.split('-').join(' ')}, please proceed to ${counterName}`;79            const utterance = new SpeechSynthesisUtterance(message);80            utterance.rate = 0.95; // Slightly slower for clear hospital announcement81            utterance.pitch = 1.0;82            83            // Animate soundwave ring in UI84            utterance.onstart = () => setSpeakingCounter(counterId);85            utterance.onend = () => setSpeakingCounter(null);86            utterance.onerror = () => setSpeakingCounter(null);87            88            window.speechSynthesis.speak(utterance);89        }90    };91 92    // Polling effects93    useEffect(() => {94        fetchQueue();95        const interval = setInterval(fetchQueue, 3000); // Poll live queue every 3s96        return () => clearInterval(interval);97    }, []);98 99    useEffect(() => {100        if (activeTab === 'analytics') {101            fetchAnalytics();102        }103    }, [activeTab]);104 105    // Token Generation (Kiosk)106    const generateToken = async (e) => {107        e.preventDefault();108        if (!patientName.trim()) return;109        110        try {111            const res = await fetch(`${API_URL}/tokens`, {112                method: 'POST',113                headers: { 'Content-Type': 'application/json' },114                body: JSON.stringify({ 115                    patient_name: patientName,116                    department: selectedDept,117                    priority: isPriority ? 1 : 0118                })119            });120            if (res.ok) {121                setPatientName('');122                setIsPriority(false);123                fetchQueue();124                125                // Show standard notification alert or just refresh126                const data = await res.json();127                alert(`Token Generated successfully!\nNumber: ${data.token_number}\nPatient: ${data.patient_name}`);128            }129        } catch (error) {130            console.error("Error generating token:", error);131        }132    };133 134    // Staff Counter Actions135    const callNext = async (counterId) => {136        try {137            const res = await fetch(`${API_URL}/counters/${counterId}/call`, { method: 'PUT' });138            if (res.ok) {139                fetchQueue();140            } else {141                alert("No waiting patients match this counter's departments.");142            }143        } catch (error) {144            console.error("Error calling next token:", error);145        }146    };147 148    const completeCurrent = async (counterId) => {149        try {150            await fetch(`${API_URL}/counters/${counterId}/complete`, { method: 'PUT' });151            fetchQueue();152        } catch (error) {153            console.error("Error completing token:", error);154        }155    };156 157    const skipCurrent = async (counterId) => {158        try {159            await fetch(`${API_URL}/counters/${counterId}/skip`, { method: 'PUT' });160            fetchQueue();161        } catch (error) {162            console.error("Error skipping token:", error);163        }164    };165 166    // Toggle Counter Online/Paused Status167    const toggleCounterStatus = async (counterId, currentStatus, supportedDepts) => {168        const nextStatus = currentStatus === 'active' ? 'paused' : 'active';169        try {170            await fetch(`${API_URL}/counters/${counterId}/status`, {171                method: 'PUT',172                headers: { 'Content-Type': 'application/json' },173                body: JSON.stringify({ 174                    status: nextStatus,175                    supported_departments: supportedDepts176                })177            });178            fetchQueue();179        } catch (error) {180            console.error("Error toggling counter status:", error);181        }182    };183 184    // Reset database to initial test state185    const resetKioskDB = async () => {186        if (confirm("Are you sure you want to reset the database? This deletes all current tokens and restores standard counter rules.")) {187            try {188                await fetch(`${API_URL}/reset`, { method: 'POST' });189                prevServingRef.current = {};190                fetchQueue();191                if (activeTab === 'analytics') fetchAnalytics();192                alert("Database reset completed successfully!");193            } catch (error) {194                console.error("Error resetting database:", error);195            }196        }197    };198 199    // Filter queue list based on department chip selection200    const filteredWaitingTokens = waitingTokens.filter(t => 201        queueFilter === 'All' || t.department === queueFilter202    );203 204    // Get max hourly count for graph scaling205    const maxHourlyCount = Math.max(...analytics.hourly_throughput.map(h => h.count), 1);206 207    return (208        <>209            <header>210                <div className="brand-section">211                    <div className="brand-logo">ๅ</div>212                    <div>213                        <h1>CareQueue</h1>214                        <span style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Clinic Token Manager</span>215                    </div>216                </div>217                218                <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>219                    <div className="nav-tabs">220                        <button 221                            className={`tab-btn ${activeTab === 'dashboard' ? 'active' : ''}`}222                            onClick={() => setActiveTab('dashboard')}223                        >224                            ๐Ÿ“Š Live Board225                        </button>226                        <button 227                            className={`tab-btn ${activeTab === 'staff' ? 'active' : ''}`}228                            onClick={() => setActiveTab('staff')}229                        >230                            ๐Ÿง‘โ€โš•๏ธ Staff Panel231                        </button>232                        <button 233                            className={`tab-btn ${activeTab === 'analytics' ? 'active' : ''}`}234                            onClick={() => setActiveTab('analytics')}235                        >236                            ๐Ÿ“ˆ Analytics237                        </button>238                    </div>239                    <button 240                        className="tab-btn" 241                        style={{ border: '1px solid rgba(244,63,94,0.3)', color: 'var(--accent-rose)' }} 242                        onClick={resetKioskDB}243                    >244                        ๐Ÿ”„ Reset245                    </button>246                </div>247            </header>248            249            <main className="container">250                {/* 1. DASHBOARD VIEW */}251                {activeTab === 'dashboard' && (252                    <div className="dashboard-layout">253                        <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>254                            {/* Serving Section */}255                            <section className="glass-panel">256                                <div className="panel-header">257                                    <h2>Now Serving</h2>258                                    <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>259                                        <span className="counter-status online"></span>260                                        <span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Live Status</span>261                                    </div>262                                </div>263                                <div className="counters-grid">264                                    {counters.map(counter => {265                                        const isOnline = counter.status === 'active';266                                        const isServing = counter.current_token;267                                        return (268                                            <div 269                                                key={counter.id} 270                                                className={`counter-card ${isServing ? 'active-serving' : ''}`}271                                                style={{ opacity: counter.status === 'offline' ? 0.4 : 1 }}272                                            >273                                                <div className="counter-header">274                                                    <span className="counter-name">{counter.name}</span>275                                                    <span className={`counter-status ${isOnline ? 'online' : 'paused'}`}></span>276                                                </div>277                                                278                                                {isServing ? (279                                                    <>280                                                        <div className="current-token pulsing">281                                                            {counter.current_token.token_number}282                                                        </div>283                                                        <div className="current-patient">284                                                            {counter.current_token.patient_name}285                                                        </div>286                                                        <div style={{ marginTop: '0.5rem', display: 'flex', justifyContent: 'center', gap: '0.5rem' }}>287                                                            <span className={`dept-badge dept-${counter.current_token.department.toLowerCase()}`}>288                                                                {counter.current_token.department}289                                                            </span>290                                                            {counter.current_token.priority === 1 && (291                                                                <span className="priority-badge">โญ VIP</span>292                                                            )}293                                                        </div>294                                                        295                                                        {speakingCounter === counter.id && (296                                                            <div style={{ marginTop: '0.75rem', display: 'flex', justifyContent: 'center' }}>297                                                                <div className="speech-pulse"></div>298                                                            </div>299                                                        )}300                                                    </>301                                                ) : (302                                                    <div className="empty-state">303                                                        {counter.status === 'paused' ? 'Paused' : 'Available'}304                                                    </div>305                                                )}306                                                307                                                <div className="supported-list" style={{ marginTop: '1rem', fontSize: '0.7rem' }}>308                                                    Route: {counter.supported_departments}309                                                </div>310                                            </div>311                                        );312                                    })}313                                </div>314                            </section>315 316                            {/* Waiting Queue List */}317                            <section className="glass-panel">318                                <div className="panel-header">319                                    <h2>Waiting Queue</h2>320                                    <span style={{ fontSize: '0.85rem', color: 'var(--accent-cyan)', fontWeight: '700' }}>321                                        {filteredWaitingTokens.length} Patients322                                    </span>323                                </div>324 325                                <div className="dept-filters">326                                    {['All', 'General', 'Pediatrics', 'Cardiology', 'Dental'].map(dept => (327                                        <div 328                                            key={dept} 329                                            className={`filter-chip ${queueFilter === dept ? 'active' : ''}`}330                                            onClick={() => setQueueFilter(dept)}331                                        >332                                            {dept}333                                        </div>334                                    ))}335                                </div>336 337                                {filteredWaitingTokens.length === 0 ? (338                                    <div className="empty-state" style={{ textAlign: 'center', padding: '3rem 0' }}>339                                        No patients waiting in {queueFilter === 'All' ? 'queue' : `${queueFilter} department`}.340                                    </div>341                                ) : (342                                    <div className="waiting-list">343                                        {filteredWaitingTokens.map(token => (344                                            <div 345                                                key={token.id} 346                                                className={`waiting-item ${token.priority === 1 ? 'priority-item' : ''}`}347                                            >348                                                <div className="token-info">349                                                    <div className="token-num">{token.token_number}</div>350                                                    <div className="patient-name-container">351                                                        <div className="patient-name">{token.patient_name}</div>352                                                        <div>353                                                            <span className={`dept-badge dept-${token.department.toLowerCase()}`}>354                                                                {token.department}355                                                            </span>356                                                        </div>357                                                    </div>358                                                </div>359                                                360                                                <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>361                                                    {token.priority === 1 && (362                                                        <span className="priority-badge">โญ Senior/Priority</span>363                                                    )}364                                                    <div className="wait-time">365                                                        <span>Est. Wait</span>366                                                        <span>{token.estimated_wait_time_mins} mins</span>367                                                    </div>368                                                </div>369                                            </div>370                                        ))}371                                    </div>372                                )}373                            </section>374                        </div>375 376                        {/* Patient Kiosk Form */}377                        <div className="actions-panel">378                            <section className="glass-panel kiosk-section">379                                <h2 style={{ marginBottom: '1.25rem' }}>๐ŸŽซ Ticket Kiosk</h2>380                                <form onSubmit={generateToken} className="kiosk-form">381                                    <div className="input-group">382                                        <label>Patient Name</label>383                                        <input 384                                            type="text" 385                                            placeholder="Enter full name" 386                                            value={patientName}387                                            onChange={(e) => setPatientName(e.target.value)}388                                            required389                                        />390                                    </div>391 392                                    <div className="input-group">393                                        <label>Select Specialty</label>394                                        <div className="kiosk-grid">395                                            {['General', 'Pediatrics', 'Cardiology', 'Dental'].map(dept => (396                                                <div 397                                                    key={dept} 398                                                    className={`dept-select-card ${selectedDept === dept ? 'selected' : ''}`}399                                                    onClick={() => setSelectedDept(dept)}400                                                >401                                                    <span>{dept}</span>402                                                </div>403                                            ))}404                                        </div>405                                    </div>406 407                                    <div className="input-group">408                                        <label className="checkbox-container">409                                            <input 410                                                type="checkbox" 411                                                checked={isPriority}412                                                onChange={(e) => setIsPriority(e.target.checked)}413                                            />414                                            Priority service (Senior Citizen / Urgency)415                                        </label>416                                    </div>417 418                                    <button type="submit" className="primary">419                                        Generate Token420                                    </button>421                                </form>422                            </section>423                        </div>424                    </div>425                )}426 427                {/* 2. STAFF WORKSPACE VIEW */}428                {activeTab === 'staff' && (429                    <div className="staff-layout">430                        <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>431                            <section className="glass-panel">432                                <div className="panel-header">433                                    <h2>Staff Counters Control</h2>434                                    <span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Manage active services</span>435                                </div>436 437                                <div className="staff-counters-grid">438                                    {counters.map(counter => {439                                        const isOnline = counter.status === 'active';440                                        const isServing = counter.current_token;441                                        return (442                                            <div key={counter.id} className="staff-counter-card">443                                                <div className="staff-card-header">444                                                    <div>445                                                        <h3 style={{ fontSize: '1.15rem' }}>{counter.name}</h3>446                                                        <span className="supported-list">447                                                            Serves: {counter.supported_departments}448                                                        </span>449                                                    </div>450                                                    <button 451                                                        className={`filter-chip ${isOnline ? 'active' : ''}`}452                                                        style={{ width: 'auto', padding: '0.25rem 0.75rem', fontSize: '0.75rem' }}453                                                        onClick={() => toggleCounterStatus(counter.id, counter.status, counter.supported_departments)}454                                                    >455                                                        {isOnline ? '๐ŸŸข Online' : '๐ŸŸก Paused'}456                                                    </button>457                                                </div>458 459                                                <div className="staff-serving-section">460                                                    {isServing ? (461                                                        <>462                                                            <div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', textTransform: 'uppercase' }}>463                                                                Serving Now464                                                            </div>465                                                            <div className="current-token" style={{ color: 'var(--accent-cyan)' }}>466                                                                {counter.current_token.token_number}467                                                            </div>468                                                            <div className="current-patient" style={{ fontWeight: '700' }}>469                                                                {counter.current_token.patient_name}470                                                            </div>471                                                        </>472                                                    ) : (473                                                        <div style={{ padding: '1rem 0', color: 'var(--text-secondary)', fontStyle: 'italic' }}>474                                                            No patient called yet475                                                        </div>476                                                    )}477                                                </div>478 479                                                <div className="staff-actions">480                                                    <button 481                                                        className="success" 482                                                        onClick={() => callNext(counter.id)}483                                                        disabled={!isOnline}484                                                        style={{ opacity: !isOnline ? 0.4 : 1, cursor: !isOnline ? 'not-allowed' : 'pointer' }}485                                                    >486                                                        Call Next487                                                    </button>488                                                    <button 489                                                        className="warning" 490                                                        onClick={() => skipCurrent(counter.id)}491                                                        disabled={!isServing}492                                                        style={{ opacity: !isServing ? 0.4 : 1, cursor: !isServing ? 'not-allowed' : 'pointer' }}493                                                    >494                                                        Skip Patient495                                                    </button>496                                                    <button 497                                                        className="danger" 498                                                        onClick={() => completeCurrent(counter.id)}499                                                        disabled={!isServing}500                                                        style={{ opacity: !isServing ? 0.4 : 1, cursor: !isServing ? 'not-allowed' : 'pointer', gridColumn: 'span 2' }}501                                                    >502                                                        Mark Completed503                                                    </button>504                                                </div>505                                            </div>506                                        );507                                    })}508                                </div>509                            </section>510                        </div>511 512                        {/* Interactive Voice Test Utility */}513                        <div className="actions-panel">514                            <section className="glass-panel">515                                <h3>๐Ÿ”Š Audio Kiosk Tester</h3>516                                <p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', margin: '0.75rem 0' }}>517                                    Staff can announce token calling using high-quality web voice synthesis. Click a sample to preview:518                                </p>519                                <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>520                                    <button className="secondary-btn" onClick={() => announceCall('GEN-001', 'Counter 1', 1)}>521                                        Call GEN-001 at Counter 1522                                    </button>523                                    <button className="secondary-btn" onClick={() => announceCall('PED-003', 'Counter 2', 2)}>524                                        Call PED-003 at Counter 2525                                    </button>526                                    <button className="secondary-btn" onClick={() => announceCall('CAR-002', 'Counter 3', 3)}>527                                        Call CAR-002 at Counter 3528                                    </button>529                                </div>530                            </section>531                        </div>532                    </div>533                )}534 535                {/* 3. ANALYTICS & REPORTS VIEW */}536                {activeTab === 'analytics' && (537                    <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>538                        {/* KPI Metrics */}539                        <section className="analytics-metrics">540                            <div className="metric-card">541                                <div className="metric-icon" style={{ background: 'rgba(6,182,212,0.12)', color: 'var(--accent-cyan)' }}>๐ŸŽซ</div>542                                <div className="metric-details">543                                    <span className="metric-value">{analytics.total_tokens_today}</span>544                                    <span className="metric-label">Total Registered</span>545                                </div>546                            </div>547                            <div className="metric-card">548                                <div className="metric-icon" style={{ background: 'rgba(16,185,129,0.12)', color: 'var(--accent-emerald)' }}>โœ…</div>549                                <div className="metric-details">550                                    <span className="metric-value">{analytics.total_completed_today}</span>551                                    <span className="metric-label">Completed</span>552                                </div>553                            </div>554                            <div className="metric-card">555                                <div className="metric-icon" style={{ background: 'rgba(245,158,11,0.12)', color: 'var(--accent-amber)' }}>โณ</div>556                                <div className="metric-details">557                                    <span className="metric-value">{analytics.avg_wait_time_mins}m</span>558                                    <span className="metric-label">Avg Wait Time</span>559                                </div>560                            </div>561                            <div className="metric-card">562                                <div className="metric-icon" style={{ background: 'rgba(244,63,94,0.12)', color: 'var(--accent-rose)' }}>๐Ÿšซ</div>563                                <div className="metric-details">564                                    <span className="metric-value">565                                        {analytics.total_skipped_today + analytics.total_cancelled_today}566                                    </span>567                                    <span className="metric-label">Skipped/Cancelled</span>568                                </div>569                            </div>570                        </section>571 572                        {/* Chart Grid */}573                        <section className="analytics-charts">574                            {/* Hourly Flow Chart */}575                            <div className="glass-panel chart-panel">576                                <h2>Hourly Flow Throughput</h2>577                                <div className="bar-chart-container">578                                    {analytics.hourly_throughput.map(item => {579                                        const percentageHeight = (item.count / maxHourlyCount) * 80; // scale to 80% max580                                        return (581                                            <div key={item.hour} className="bar-col">582                                                <div 583                                                    className="bar-fill" 584                                                    style={{ height: `${Math.max(percentageHeight, 4)}%` }}585                                                >586                                                    <div className="bar-tooltip">{item.count} tickets</div>587                                                </div>588                                                <span className="bar-label">{item.hour}</span>589                                            </div>590                                        );591                                    })}592                                </div>593                            </div>594 595                            {/* Department Distribution Chart */}596                            <div className="glass-panel chart-panel">597                                <h2>Department Routing Share</h2>598                                <div className="pie-chart-list">599                                    {analytics.by_department.map(item => {600                                        const percentage = analytics.total_tokens_today > 0 601                                            ? Math.round((item.count / analytics.total_tokens_today) * 100) 602                                            : 0;603                                        604                                        const colorDot = {605                                            'General': 'var(--accent-cyan)',606                                            'Pediatrics': 'var(--accent-emerald)',607                                            'Cardiology': 'var(--accent-rose)',608                                            'Dental': 'var(--accent-amber)'609                                        }[item.department] || 'var(--text-secondary)';610 611                                        return (612                                            <div key={item.department} className="pie-chart-row">613                                                <div className="dept-name-wrapper">614                                                    <span className="dept-color-dot" style={{ background: colorDot }}></span>615                                                    <span style={{ fontWeight: '600' }}>{item.department}</span>616                                                </div>617                                                <div className="chart-bar-bg">618                                                    <div 619                                                        className="chart-bar-fill" 620                                                        style={{ 621                                                            width: `${percentage}%`, 622                                                            background: colorDot 623                                                        }}624                                                    ></div>625                                                </div>626                                                <div style={{ minWidth: '60px', textAlign: 'right' }}>627                                                    <span style={{ fontWeight: '700' }}>{item.count}</span>628                                                    <span style={{ color: 'var(--text-secondary)', fontSize: '0.75rem', marginLeft: '0.25rem' }}>629                                                        ({percentage}%)630                                                    </span>631                                                </div>632                                            </div>633                                        );634                                    })}635                                </div>636                            </div>637                        </section>638 639                        {/* Export & Data Report Details */}640                        <section className="glass-panel" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>641                            <div>642                                <h3>๐Ÿ“Š Daily Logs Report</h3>643                                <p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>644                                    Export all register records, serving times, department statistics, and wait estimations as a CSV file.645                                </p>646                            </div>647                            <a 648                                href={`${API_URL}/analytics/export`} 649                                target="_blank"650                                download651                                className="tab-btn active"652                                style={{ textDecoration: 'none', height: 'auto', display: 'inline-flex', alignItems: 'center', background: 'linear-gradient(135deg, var(--accent-cyan), var(--accent-emerald))', padding: '0.8rem 1.5rem', borderRadius: '0.75rem' }}653                            >654                                ๐Ÿ“ฅ Export CSV Report655                            </a>656                        </section>657                    </div>658                )}659            </main>660        </>661    );662}663 664const root = ReactDOM.createRoot(document.getElementById('root'));665root.render(<App />);666 667