Spaces:
Running
Running
| <html lang="fa" dir="rtl"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>نرمافزار مدیریت آزمایشگاه آب و خاک - آزمایشگاه اهواز</title> | |
| <link href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css" rel="stylesheet"> | |
| <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/jalaali-js@1.1.3/dist/jalaali.min.js"></script> | |
| <style> | |
| :root{ | |
| --bg: #f6f7fb; | |
| --card: #ffffff; | |
| --primary: #0061ff; | |
| --primary-600:#0053d6; | |
| --primary-50:#e6f0ff; | |
| --success:#10b981; | |
| --success-600:#059669; | |
| --warning:#f59e0b; | |
| --danger:#ef4444; | |
| --text:#1f2937; | |
| --muted:#6b7280; | |
| --border:#e5e7eb; | |
| --shadow: 0 10px 25px rgba(0,0,0,0.07); | |
| --radius: 16px; | |
| --soil: #8B4513; | |
| --soil-50: #F4E4D4; | |
| --water: #0EA5E9; | |
| --water-50: #E0F2FE; | |
| } | |
| *{ box-sizing:border-box } | |
| html,body{ height:100% } | |
| body{ | |
| margin:0; | |
| font-family: "Vazirmatn", "IRANSans", Tahoma, Arial, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| a{ color: var(--primary); text-decoration: none } | |
| header{ | |
| position: sticky; top:0; z-index: 50; | |
| backdrop-filter: saturate(180%) blur(12px); | |
| background: rgba(255,255,255,0.85); | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .topbar{ | |
| max-width: 1400px; margin: 0 auto; padding: 12px 20px; | |
| display: flex; align-items: center; justify-content: space-between; gap: 12px; | |
| } | |
| .brand{ | |
| display: flex; align-items: center; gap: 12px; | |
| } | |
| .brand .logo{ | |
| width: 40px; height: 40px; border-radius: 12px; | |
| background: linear-gradient(135deg, var(--primary), var(--soil)); | |
| display: grid; place-items: center; color: white; font-weight: 800; | |
| box-shadow: var(--shadow); | |
| } | |
| .brand .title{ | |
| display: flex; flex-direction: column; line-height: 1.2; | |
| } | |
| .brand .title b{ font-size: 18px } | |
| .brand .title small{ color: var(--muted) } | |
| .actions{ display: flex; gap: 10px; align-items: center } | |
| .badge{ | |
| display: inline-flex; align-items: center; gap: 6px; | |
| padding: 6px 10px; border-radius: 999px; font-size: 13px; | |
| border: 1px solid var(--border); background: var(--card); | |
| } | |
| .badge.success{ color: var(--success-600); background: #ecfdf5; border-color:#d1fae5 } | |
| .badge.warn{ color: #b45309; background: #fffbeb; border-color:#fde68a } | |
| .badge.danger{ color: var(--danger); background: #fef2f2; border-color:#fee2e2 } | |
| .badge.iso{ color: var(--primary-600); background: var(--primary-50); border-color:#cfe0ff } | |
| .user{ | |
| display:flex; align-items:center; gap:10px; padding:6px 10px; border-radius: 999px; | |
| background: var(--card); border: 1px solid var(--border); | |
| } | |
| .user i{ font-size: 18px } | |
| nav{ | |
| border-top: 1px solid var(--border); | |
| background: white; | |
| } | |
| .tabs{ | |
| max-width: 1400px; margin: 0 auto; | |
| display:flex; gap: 8px; padding: 10px 20px; overflow:auto; | |
| } | |
| .tab{ | |
| display:inline-flex; align-items:center; gap:8px; | |
| padding: 10px 14px; border-radius: 12px; | |
| color: var(--muted); cursor: pointer; border: 1px solid transparent; | |
| } | |
| .tab.active{ | |
| color: var(--primary); background: var(--primary-50); border-color:#cfe0ff; | |
| } | |
| main{ max-width: 1400px; margin: 18px auto; padding: 0 20px 40px } | |
| .grid{ | |
| display: grid; gap: 18px; | |
| } | |
| @media (min-width: 900px){ | |
| .grid.cols-4{ grid-template-columns: repeat(4, 1fr) } | |
| .grid.cols-3{ grid-template-columns: repeat(3, 1fr) } | |
| .grid.cols-2{ grid-template-columns: 2fr 1fr } | |
| } | |
| .card{ | |
| background: var(--card); border: 1px solid var(--border); | |
| border-radius: var(--radius); box-shadow: var(--shadow); | |
| padding: 16px; | |
| } | |
| .card h3{ margin: 0 0 10px; font-size: 18px } | |
| .kpi{ | |
| display:flex; align-items:center; justify-content: space-between; | |
| gap: 14px; padding: 16px; border-radius: 14px; | |
| background: linear-gradient(180deg, #ffffff, #f8fafc); | |
| border: 1px solid var(--border); | |
| } | |
| .kpi .val{ font-size: 32px; font-weight: 800 } | |
| .kpi .label{ color: var(--muted); font-size: 14px } | |
| .kpi .icon{ | |
| width: 46px; height: 46px; border-radius: 12px; display:grid; place-items:center; | |
| color: white; font-size: 22px; font-weight: 700; background: var(--primary); | |
| box-shadow: var(--shadow); | |
| } | |
| .kpi.soil .icon{ background: var(--soil) } | |
| .kpi.water .icon{ background: var(--water) } | |
| .section-title{ | |
| display:flex; align-items:center; justify-content: space-between; margin-bottom: 10px; | |
| } | |
| .btn{ | |
| display:inline-flex; align-items:center; gap:8px; | |
| padding: 10px 14px; border-radius: 12px; border: 1px solid var(--border); | |
| background: white; color: var(--text); cursor:pointer; | |
| } | |
| .btn.primary{ | |
| background: var(--primary); color: white; border-color: transparent; | |
| } | |
| .btn.soil{ background: var(--soil); color: white; border-color: transparent } | |
| .btn.water{ background: var(--water); color: white; border-color: transparent } | |
| .btn.primary:hover{ background: var(--primary-600) } | |
| .btn.ghost:hover{ background: #f3f4f6 } | |
| .btn.success{ background: var(--success); color: white; border-color: transparent } | |
| .btn.success:hover{ background: #059669 } | |
| .btn:disabled{ opacity: .6; cursor:not-allowed } | |
| .table{ | |
| width:100%; border-collapse: collapse; font-size: 14px; | |
| } | |
| .table th, .table td{ | |
| padding: 12px; border-bottom: 1px solid var(--border); text-align: right; | |
| } | |
| .status{ | |
| padding: 6px 10px; border-radius: 999px; font-size: 12px; display:inline-block; | |
| } | |
| .status.received{ background: #eef2ff; color:#3730a3 } | |
| .status.intest{ background: #ecfeff; color:#0e7490 } | |
| .status.done{ background: #ecfdf5; color:#065f46 } | |
| .status.approved{ background: #fef3c7; color:#92400e } | |
| .muted{ color: var(--muted) } | |
| .sample-type{ display: inline-flex; align-items: center; gap: 6px } | |
| .sample-type.soil{ color: var(--soil) } | |
| .sample-type.water{ color: var(--water) } | |
| /* Sample Registration */ | |
| .sample-form{ | |
| display: grid; gap: 16px; | |
| } | |
| .form-group{ | |
| display: grid; gap: 6px; | |
| } | |
| .form-group label{ | |
| font-size: 14px; font-weight: 600; color: var(--text); | |
| } | |
| .form-group input, .form-group select, .form-group textarea{ | |
| padding: 10px 12px; border: 1px solid var(--border); border-radius: 12px; | |
| font-size: 14px; font-family: inherit; | |
| } | |
| .form-group textarea{ resize: vertical; min-height: 80px } | |
| .form-row{ | |
| display: grid; gap: 16px; | |
| } | |
| @media (min-width: 768px){ | |
| .form-row{ grid-template-columns: repeat(2, 1fr) } | |
| } | |
| /* Instruments */ | |
| .instrument-card{ | |
| display: grid; gap: 12px; | |
| padding: 16px; border: 1px solid var(--border); border-radius: 12px; | |
| background: white; | |
| } | |
| .instrument-header{ | |
| display: flex; align-items: center; justify-content: space-between; | |
| } | |
| .instrument-status{ | |
| display: inline-flex; align-items: center; gap: 6px; | |
| padding: 4px 8px; border-radius: 8px; font-size: 12px; | |
| } | |
| .instrument-status.active{ background: #ecfdf5; color:#065f46 } | |
| .instrument-status.maintenance{ background: #fef3c7; color:#92400e } | |
| /* Audit Trail */ | |
| .audit-item{ | |
| display: flex; gap: 12px; padding: 12px; border-radius: 8px; | |
| background: #f8fafc; border-left: 3px solid var(--primary); | |
| } | |
| .audit-time{ color: var(--muted); font-size: 12px } | |
| /* Charts */ | |
| .chart-container{ | |
| position: relative; height: 300px; | |
| } | |
| /* ISO Badge */ | |
| .iso-compliance{ | |
| display: inline-flex; align-items: center; gap: 8px; | |
| padding: 8px 12px; border-radius: 12px; | |
| background: linear-gradient(135deg, var(--primary-50), white); | |
| border: 1px solid var(--primary-200); | |
| } | |
| /* Test Results */ | |
| .test-results{ | |
| display: grid; gap: 12px; | |
| } | |
| .test-item{ | |
| padding: 16px; border: 1px solid var(--border); border-radius: 12px; | |
| background: white; | |
| } | |
| .parameter-grid{ | |
| display: grid; gap: 8px; margin-top: 12px; | |
| } | |
| .parameter-row{ | |
| display: flex; justify-content: space-between; align-items: center; | |
| padding: 8px 12px; background: #f8fafc; border-radius: 8px; | |
| } | |
| .parameter-value{ font-weight: 700 } | |
| /* Responsive */ | |
| @media (max-width: 768px){ | |
| .grid.cols-4{ grid-template-columns: repeat(2, 1fr) } | |
| .tabs{ padding: 10px 12px } | |
| main{ padding: 0 12px 40px } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="topbar"> | |
| <div class="brand"> | |
| <div class="logo">LIMS</div> | |
| <div class="title"> | |
| <b>نرمافزار مدیریت آزمایشگاه آب و خاک - اهواز</b> | |
| <small>سیستم مدیریت اطلاعات آزمایشگاهی • انطباق ISO 17025</small> | |
| </div> | |
| </div> | |
| <div class="actions"> | |
| <span class="badge iso"><i class="ri-award-line"></i> ISO 17025</span> | |
| <span class="badge success" id="systemStatus"><i class="ri-shield-check-line"></i> سیستم فعال</span> | |
| <span class="badge" id="todayBadge"><i class="ri-time-line"></i></span> | |
| <div class="user"> | |
| <i class="ri-admin-line"></i> | |
| <span id="currentUser">مدیر سیستم</span> | |
| </div> | |
| </div> | |
| </div> | |
| <nav> | |
| <div class="tabs"> | |
| <a class="tab active" data-view="dashboard"><i class="ri-dashboard-line"></i> داشبورد</a> | |
| <a class="tab" data-view="samples"><i class="ri-flask-line"></i> مدیریت نمونهها</a> | |
| <a class="tab" data-view="instruments"><i class="ri-tools-line"></i> تجهیزات</a> | |
| <a class="tab" data-view="tests"><i class="ri-test-tube-line"></i> آزمونها</a> | |
| <a class="tab" data-view="reports"><i class="ri-file-chart-line"></i> گزارشها</a> | |
| <a class="tab" data-view="audit"><i class="ri-history-line"></i> بازرسی</a> | |
| </div> | |
| </nav> | |
| </header> | |
| <main> | |
| <!-- DASHBOARD --> | |
| <section id="view-dashboard"> | |
| <div class="grid cols-4"> | |
| <div class="kpi card"> | |
| <div> | |
| <div class="label">کل نمونهها</div> | |
| <div class="val" id="totalSamples">0</div> | |
| </div> | |
| <div class="icon"><i class="ri-flask-line"></i></div> | |
| </div> | |
| <div class="kpi card soil"> | |
| <div> | |
| <div class="label">نمونههای خاک</div> | |
| <div class="val" id="soilSamples">0</div> | |
| </div> | |
| <div class="icon"><i class="ri-landscape-line"></i></div> | |
| </div> | |
| <div class="kpi card water"> | |
| <div> | |
| <div class="label">نمونههای آب</div> | |
| <div class="val" id="waterSamples">0</div> | |
| </div> | |
| <div class="icon"><i class="ri-drop-line"></i></div> | |
| </div> | |
| <div class="kpi card"> | |
| <div> | |
| <div class="label">آزمونهای تکمیل</div> | |
| <div class="val" id="completedTests">0</div> | |
| </div> | |
| <div class="icon" style="background: var(--success)"><i class="ri-check-double-line"></i></div> | |
| </div> | |
| </div> | |
| <div class="grid cols-2" style="margin-top: 18px"> | |
| <div class="card"> | |
| <h3><i class="ri-bar-chart-2-line"></i> روند پارامترها</h3> | |
| <div class="chart-container"> | |
| <canvas id="trendChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3><i class="ri-notification-3-line"></i> فعالیتهای اخیر</h3> | |
| <div id="recentActivities" style="max-height: 300px; overflow-y: auto"></div> | |
| </div> | |
| </div> | |
| <div class="card" style="margin-top: 18px"> | |
| <div class="section-title"> | |
| <h3><i class="ri-list-unordered"></i> نمونههای اخیر</h3> | |
| <button class="btn ghost" onclick="gotoView('samples')"><i class="ri-arrow-right-up-line"></i> مشاهده همه</button> | |
| </div> | |
| <div style="overflow-x: auto"> | |
| <table class="table" id="recentSamplesTable"> | |
| <thead> | |
| <tr> | |
| <th>شناسه نمونه</th> | |
| <th>نوع نمونه</th> | |
| <th>مشتری</th> | |
| <th>محصول</th> | |
| <th>وضعیت</th> | |
| <th>تاریخ ثبت</th> | |
| <th>عملیات</th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- SAMPLES --> | |
| <section id="view-samples" style="display:none"> | |
| <div class="card"> | |
| <div class="section-title"> | |
| <h3><i class="ri-flask-line"></i> مدیریت نمونهها</h3> | |
| <button class="btn primary" onclick="showSampleForm()"><i class="ri-add-line"></i> ثبت نمونه جدید</button> | |
| </div> | |
| <div id="sampleForm" style="display:none; margin-top: 20px"> | |
| <div class="sample-form"> | |
| <h4><i class="ri-file-add-line"></i> فرم ثبت نمونه</h4> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>شناسه نمونه</label> | |
| <input type="text" id="sampleId" readonly style="background: #f8fafc"> | |
| </div> | |
| <div class="form-group"> | |
| <label>نوع نمونه</label> | |
| <select id="sampleType"> | |
| <option value="soil">خاک</option> | |
| <option value="water">آب</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>نام مشتری</label> | |
| <input type="text" id="clientName" placeholder="نام مشتری یا سازمان"> | |
| </div> | |
| <div class="form-group"> | |
| <label>محصول/منبع</label> | |
| <input type="text" id="product" placeholder="نوع محصول یا منبع آب"> | |
| </div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>مکان نمونهبرداری</label> | |
| <input type="text" id="location"> | |
| </div> | |
| <div class="form-group"> | |
| <label>تاریخ نمونهبرداری</label> | |
| <input type="date" id="collectionDate"> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>آزمونهای درخواستی</label> | |
| <div style="display: flex; gap: 12px; flex-wrap: wrap"> | |
| <label><input type="checkbox" value="pH"> pH</label> | |
| <label><input type="checkbox" value="EC"> EC</label> | |
| <label><input type="checkbox" value="N"> نیتروژن (N)</label> | |
| <label><input type="checkbox" value="P"> فسفر (P)</label> | |
| <label><input type="checkbox" value="K"> پتاسیم (K)</label> | |
| <label><input type="checkbox" value="organic"> مواد آلی</label> | |
| <label><input type="checkbox" value="texture"> بافت خاک</label> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>یادداشتها</label> | |
| <textarea id="notes" placeholder="توضیحات اضافی..."></textarea> | |
| </div> | |
| <div style="display: flex; gap: 12px"> | |
| <button class="btn primary" onclick="registerSample()"><i class="ri-save-line"></i> ثبت نمونه</button> | |
| <button class="btn ghost" onclick="hideSampleForm()"><i class="ri-close-line"></i> انصراف</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="overflow-x: auto; margin-top: 20px"> | |
| <table class="table" id="samplesTable"> | |
| <thead> | |
| <tr> | |
| <th>شناسه نمونه</th> | |
| <th>نوع نمونه</th> | |
| <th>مشتری</th> | |
| <th>محصول/منبع</th> | |
| <th>مکان</th> | |
| <th>آزمونها</th> | |
| <th>وضعیت</th> | |
| <th>عملیات</th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- INSTRUMENTS --> | |
| <section id="view-instruments" style="display:none"> | |
| <div class="card"> | |
| <div class="section-title"> | |
| <h3><i class="ri-tools-line"></i> مدیریت تجهیزات</h3> | |
| <button class="btn primary" onclick="addInstrument()"><i class="ri-add-line"></i> افزودن تجهیزات</button> | |
| </div> | |
| <div class="grid cols-3" id="instrumentsGrid"></div> | |
| </div> | |
| </section> | |
| <!-- TESTS --> | |
| <section id="view-tests" style="display:none"> | |
| <div class="card"> | |
| <div class="section-title"> | |
| <h3><i class="ri-test-tube-line"></i> اجرای آزمونها</h3> | |
| </div> | |
| <div id="testExecution"></div> | |
| </div> | |
| </section> | |
| <!-- REPORTS --> | |
| <section id="view-reports" style="display:none"> | |
| <div class="card"> | |
| <div class="section-title"> | |
| <h3><i class="ri-file-chart-line"></i> گزارشهای آزمایشگاهی</h3> | |
| <div> | |
| <button class="btn primary" onclick="generateReport()"><i class="ri-download-2-line"></i> تولید گزارش PDF</button> | |
| <button class="btn ghost" onclick="window.print()"><i class="ri-printer-line"></i> چاپ</button> | |
| </div> | |
| </div> | |
| <div id="reportContent"></div> | |
| </div> | |
| </section> | |
| <!-- AUDIT TRAIL --> | |
| <section id="view-audit" style="display:none"> | |
| <div class="card"> | |
| <div class="section-title"> | |
| <h3><i class="ri-history-line"></i> سوابق و بازرسی (Audit Trail)</h3> | |
| <div class="iso-compliance"> | |
| <i class="ri-shield-check-line"></i> | |
| <span>انطباق کامل با ISO 17025</span> | |
| </div> | |
| </div> | |
| <div id="auditTrail"></div> | |
| </div> | |
| </section> | |
| </main> | |
| <footer> | |
| © ۱۴۰۴ – آزمایشگاه آب و خاک اهواز • سیستم مدیریت اطلاعات آزمایشگاهی (LIMS) • Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </footer> | |
| <script> | |
| // ---------- Localization helpers ---------- | |
| const fa = (n) => new Intl.NumberFormat('fa-IR').format(n); | |
| function toPersianDigits(str){ return String(str).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d]) } | |
| function nowJalaliString(){ | |
| const d = new Date(); | |
| const j = jalaali(d.getFullYear(), d.getMonth()+1, d.getDate()); | |
| return `${j.jy}/${j.jm}/${j.jd} ${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}`; | |
| } | |
| function timeAgo(ts){ | |
| const d = Math.floor((Date.now() - ts)/1000); | |
| if (d < 60) return `${d} ثانیه پیش`; | |
| const m = Math.floor(d/60); if (m < 60) return `${m} دقیقه پیش`; | |
| const h = Math.floor(m/60); if (h < 24) return `${h} ساعت پیش`; | |
| const day = Math.floor(h/24); return `${day} روز پیش`; | |
| } | |
| // ---------- App State ---------- | |
| const State = { | |
| currentUser: { userId: 'U001', fullName: 'مدیر سیستم', role: 'Admin' }, | |
| samples: [], | |
| instruments: [], | |
| tests: [], | |
| reports: [], | |
| auditTrail: [], | |
| charts: {} | |
| }; | |
| // ---------- Initialize Data ---------- | |
| function initializeData(){ | |
| // Add sample data from JSON | |
| State.samples.push({ | |
| sampleId: 'SAMPLE_DKH_2025_001', | |
| type: 'soil', | |
| clientName: 'کشت و صنعت دهخدا', | |
| product: 'نیشکر', | |
| collectionDate: '2025-09-28', | |
| location: 'مزرعه شماره 12 - دهخدا', | |
| gps: { latitude: 31.318, longitude: 48.670 }, | |
| status: 'Received', | |
| requestedTests: ['pH', 'EC', 'N', 'P', 'K'], | |
| tests: [ | |
| { | |
| testId: 'T001', | |
| testName: 'Ph و Conductivity', | |
| methodCode: 'STD_SOIL_001', | |
| results: [ | |
| { parameter: 'pH', value: 7.5, unit: '' }, | |
| { parameter: 'EC', value: 1200, unit: 'µS/cm' } | |
| ], | |
| instrument: 'EC/pH Meter', | |
| performedBy: 'Technician1', | |
| performedDate: '2025-09-29' | |
| }, | |
| { | |
| testId: 'T002', | |
| testName: 'NPK عناصر غذایی', | |
| methodCode: 'STD_SOIL_002', | |
| results: [ | |
| { parameter: 'N', value: 0.18, unit: '%' }, | |
| { parameter: 'P', value: 22, unit: 'mg/kg' }, | |
| { parameter: 'K', value: 180, unit: 'mg/kg' } | |
| ], | |
| instrument: 'Spectrophotometer', | |
| performedBy: 'Technician2', | |
| performedDate: '2025-09-29' | |
| } | |
| ], | |
| createdAt: Date.now() - (1000*60*60*24*2) | |
| }); | |
| // Add instruments | |
| State.instruments = [ | |
| { | |
| instrumentId: 'I001', | |
| name: 'EC/pH Meter', | |
| type: 'Conductivity & pH', | |
| status: 'Active', | |
| calibration: '2025-08-01', | |
| nextCalibration: '2025-11-01' | |
| }, | |
| { | |
| instrumentId: 'I002', | |
| name: 'Spectrophotometer', | |
| type: 'Spectrophotometer', | |
| status: 'Active', | |
| calibration: '2025-07-15', | |
| nextCalibration: '2025-10-15' | |
| } | |
| ]; | |
| // Add audit trail entries | |
| State.auditTrail = [ | |
| { | |
| action: 'registered', | |
| by: 'Admin', | |
| timestamp: '2025-09-28T10:00:00Z', | |
| details: 'ثبت نمونه SAMPLE_DKH_2025_001' | |
| }, | |
| { | |
| action: 'test_completed', | |
| by: 'Technician1', | |
| timestamp: '2025-09-29T14:30:00Z', | |
| details: 'تکمیل آزمون pH و EC برای نمونه SAMPLE_DKH_2025_001' | |
| } | |
| ]; | |
| updateDashboard(); | |
| } | |
| // ---------- Navigation ---------- | |
| const views = ['dashboard','samples','instruments','tests','reports','audit']; | |
| function gotoView(view){ | |
| views.forEach(v=>{ | |
| document.getElementById('view-'+v).style.display = (v===view)?'block':'none'; | |
| document.querySelectorAll('.tab').forEach(t=>{ | |
| if (t.dataset.view===view) t.classList.add('active'); else t.classList.remove('active'); | |
| }); | |
| }); | |
| if (view==='dashboard') updateDashboard(); | |
| if (view==='samples') renderSamples(); | |
| if (view==='instruments') renderInstruments(); | |
| if (view==='audit') renderAuditTrail(); | |
| } | |
| document.querySelectorAll('.tab').forEach(t=>{ | |
| t.addEventListener('click', ()=> gotoView(t.dataset.view)); | |
| }); | |
| // ---------- Dashboard ---------- | |
| function updateDashboard(){ | |
| const totalSamples = State.samples.length; | |
| const soilSamples = State.samples.filter(s => s.type === 'soil').length; | |
| const waterSamples = State.samples.filter(s => s.type === 'water').length; | |
| const completedTests = State.samples.reduce((acc, s) => acc + (s.tests?.length || 0), 0); | |
| document.getElementById('totalSamples').innerText = toPersianDigits(totalSamples); | |
| document.getElementById('soilSamples').innerText = toPersianDigits(soilSamples); | |
| document.getElementById('waterSamples').innerText = toPersianDigits(waterSamples); | |
| document.getElementById('completedTests').innerText = toPersianDigits(completedTests); | |
| // Update recent samples table | |
| const tbody = document.querySelector('#recentSamplesTable tbody'); | |
| tbody.innerHTML = ''; | |
| const recentSamples = [...State.samples].sort((a,b) => b.createdAt - a.createdAt).slice(0,5); | |
| recentSamples.forEach(s => { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td>${s.sampleId}</td> | |
| <td><span class="sample-type ${s.type}"><i class="ri-${s.type === 'soil' ? 'landscape' : 'drop'}-line"></i> ${s.type === 'soil' ? 'خاک' : 'آب'}</span></td> | |
| <td>${s.clientName}</td> | |
| <td>${s.product || '-'}</td> | |
| <td>${getStatusBadge(s.status)}</td> | |
| <td>${s.collectionDate}</td> | |
| <td><button class="btn ghost" onclick="viewSampleDetails('${s.sampleId}')"><i class="ri-eye-line"></i></button></td> | |
| `; | |
| tbody.appendChild(tr); | |
| }); | |
| // Update recent activities | |
| const activitiesDiv = document.getElementById('recentActivities'); | |
| activitiesDiv.innerHTML = ''; | |
| State.auditTrail.slice(0,5).forEach(a => { | |
| const div = document.createElement('div'); | |
| div.className = 'audit-item'; | |
| div.innerHTML = ` | |
| <div> | |
| <div>${a.details}</div> | |
| <div class="audit-time">توسط ${a.by} • ${new Date(a.timestamp).toLocaleString('fa-IR')}</div> | |
| </div> | |
| `; | |
| activitiesDiv.appendChild(div); | |
| }); | |
| // Initialize trend chart | |
| initializeTrendChart(); | |
| document.getElementById('todayBadge').innerHTML = `<i class="ri-time-line"></i> ${nowJalaliString()}`; | |
| } | |
| function getStatusBadge(status){ | |
| const badges = { | |
| 'Received': '<span class="status received">دریافتشده</span>', | |
| 'In Progress': '<span class="status intest">در حال انجام</span>', | |
| 'Completed': '<span class="status done">تکمیلشده</span>', | |
| 'Approved': '<span class="status approved">تایید شده</span>' | |
| }; | |
| return badges[status] || status; | |
| } | |
| // ---------- Trend Chart ---------- | |
| function initializeTrendChart(){ | |
| const ctx = document.getElementById('trendChart'); | |
| if (!ctx) return; | |
| if (State.charts.trend) State.charts.trend.destroy(); | |
| State.charts.trend = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: ['شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن'], | |
| datasets: [ | |
| { | |
| label: 'pH', | |
| data: [7.2, 7.4, 7.1, 7.5, 7.3, 7.5], | |
| borderColor: '#0061ff', | |
| tension: 0.4 | |
| }, | |
| { | |
| label: 'EC (µS/cm)', | |
| data: [1100, 1150, 1200, 1180, 1220, 1200], | |
| borderColor: '#10b981', | |
| tension: 0.4 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'top', | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // ---------- Samples Management ---------- | |
| function showSampleForm(){ | |
| document.getElementById('sampleForm').style.display = 'block'; | |
| // Generate sample ID | |
| const year = new Date().getFullYear(); | |
| const random = Math.floor(Math.random()*1000).toString().padStart(3,'0'); | |
| document.getElementById('sampleId').value = `SAMPLE_AHV_${year}_${random}`; | |
| // Set today's date | |
| document.getElementById('collectionDate').value = new Date().toISOString().split('T')[0]; | |
| } | |
| function hideSampleForm(){ | |
| document.getElementById('sampleForm').style.display = 'none'; | |
| } | |
| function registerSample(){ | |
| const sample = { | |
| sampleId: document.getElementById('sampleId').value, | |
| type: document.getElementById('sampleType').value, | |
| clientName: document.getElementById('clientName').value, | |
| product: document.getElementById('product').value, | |
| location: document.getElementById('location').value, | |
| collectionDate: document.getElementById('collectionDate').value, | |
| status: 'Received', | |
| requestedTests: Array.from(document.querySelectorAll('#sampleForm input[type="checkbox"]:checked')).map(cb => cb.value), | |
| notes: document.getElementById('notes').value, | |
| createdAt: Date.now(), | |
| tests: [] | |
| }; | |
| State.samples.unshift(sample); | |
| // Add to audit trail | |
| addAuditEntry('registered', State.currentUser.fullName, `ثبت نمونه ${sample.sampleId}`); | |
| hideSampleForm(); | |
| renderSamples(); | |
| updateDashboard(); | |
| } | |
| function renderSamples(){ | |
| const tbody = document.querySelector('#samplesTable tbody'); | |
| tbody.innerHTML = ''; | |
| State.samples.forEach(s => { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td>${s.sampleId}</td> | |
| <td><span class="sample-type ${s.type}"><i class="ri-${s.type === 'soil' ? 'landscape' : 'drop'}-line"></i> ${s.type === 'soil' ? 'خاک' : 'آب'}</span></td> | |
| <td>${s.clientName}</td> | |
| <td>${s.product || '-'}</td> | |
| <td>${s.location}</td> | |
| <td |