| | <!DOCTYPE html> |
| | <html lang="fa" dir="rtl"> |
| | <head> |
| | <meta charset="utf-8" /> |
| | <meta name="viewport" content="width=device-width,initial-scale=1" /> |
| | <title>سامانه بازرسی و پلاک برداری از کارگاه ها — اجرا محلی با Fallback مختصات</title> |
| | <style> |
| | :root{ |
| | --p:#4361ee; |
| | --p2:#3f37c9; |
| | --p-light:#e0e7ff; |
| | --p-dark:#3a0ca3; |
| | --s:#2a9d8f; |
| | --w:#e63946; |
| | --y:#f77f00; |
| | --muted:#6c757d; |
| | --bg:#f8f9fa; |
| | --card:#ffffff; |
| | --text:#212529; |
| | --text-light:#6c757d; |
| | --border:#dee2e6; |
| | --shadow:0 4px 6px rgba(0,0,0,0.05), 0 1px 3px rgba(0,0,0,0.1), 0 0 0 1px rgba(255,255,255,0.05); |
| | --shadow-hover:0 10px 15px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05); |
| | --shadow-card:0 10px 15px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05); |
| | --shadow-modal:0 20px 25px rgba(0,0,0,0.15), 0 10px 10px rgba(0,0,0,0.04); |
| | --radius:16px; |
| | --transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| | --gradient-primary:linear-gradient(135deg, var(--p), var(--p2)); |
| | --gradient-secondary:linear-gradient(135deg, #667eea, #764ba2); |
| | --gradient-success:linear-gradient(135deg, #2a9d8f, #1d7a73); |
| | --gradient-danger:linear-gradient(135deg, #e63946, #d62828); |
| | --gradient-warning:linear-gradient(135deg, #f77f00, #fcbf49); |
| | --gradient-manager:linear-gradient(135deg, #1e3a8a, #1e40af, #2563eb); |
| | } |
| | |
| | *{box-sizing:border-box;margin:0;padding:0} |
| | |
| | body{ |
| | font-family:'Yekan Bakh', 'Tahoma', sans-serif; |
| | direction:rtl; |
| | margin:0; |
| | background:linear-gradient(165deg, #f5f7fa 0%, #e4efe9 100%); |
| | color:var(--text); |
| | line-height:1.6; |
| | overflow-x:hidden; |
| | min-height:100vh; |
| | } |
| | |
| | body.modal-open { overflow: hidden; } |
| | |
| | /* بهبود تایپوگرافی */ |
| | h1, h2, h3, h4, h5, h6 { |
| | font-weight:800; |
| | line-height:1.2; |
| | margin-bottom:0.5em; |
| | color:var(--p-dark); |
| | letter-spacing: -0.02em; |
| | } |
| | |
| | /* بهبود هدر */ |
| | header{ |
| | background:var(--gradient-primary); |
| | color:#fff; |
| | padding:24px 32px; |
| | display:flex; |
| | justify-content:space-between; |
| | align-items:center; |
| | box-shadow:var(--shadow-card); |
| | position:relative; |
| | z-index:10; |
| | border-bottom-left-radius: 24px; |
| | border-bottom-right-radius: 24px; |
| | overflow: hidden; |
| | } |
| | |
| | header::before { |
| | content: ''; |
| | position: absolute; |
| | top: 0; |
| | left: 0; |
| | right: 0; |
| | bottom: 0; |
| | background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="none"/><path d="M0,0 L100,100 M100,0 L0,100" stroke="rgba(255,255,255,0.05)" stroke-width="1"/></svg>'); |
| | opacity: 0.4; |
| | z-index: 0; |
| | } |
| | |
| | header h1{ |
| | margin:0; |
| | font-size:1.8rem; |
| | font-weight:800; |
| | text-shadow:0 2px 4px rgba(0,0,0,0.2); |
| | position: relative; |
| | z-index: 1; |
| | } |
| | |
| | header .author{ |
| | font-size:1rem; |
| | opacity:0.95; |
| | text-align:right; |
| | position: relative; |
| | z-index: 1; |
| | } |
| | |
| | /* بهبود دکمه ذخیره دادهها */ |
| | #btnSaveData { |
| | background-color: rgba(255,255,255,0.2); |
| | color: white; |
| | border: 1px solid rgba(255,255,255,0.3); |
| | padding: 10px 20px; |
| | border-radius: 30px; |
| | font-weight: 700; |
| | transition: var(--transition); |
| | margin-right: 16px; |
| | box-shadow: 0 4px 6px rgba(0,0,0,0.1); |
| | backdrop-filter: blur(4px); |
| | } |
| | |
| | #btnSaveData:hover { |
| | background-color: rgba(255,255,255,0.3); |
| | transform: translateY(-2px); |
| | box-shadow: 0 8px 15px rgba(0,0,0,0.2); |
| | } |
| | |
| | main{max-width:1200px;margin:32px auto;padding:24px;position:relative;overflow-x:hidden} |
| | |
| | /* بهبود کارتها */ |
| | .card{ |
| | background:var(--card); |
| | border-radius:20px; |
| | padding:28px; |
| | box-shadow:var(--shadow-card); |
| | margin-bottom:28px; |
| | transition:var(--transition); |
| | border: 1px solid rgba(255,255,255,0.8); |
| | position: relative; |
| | overflow: hidden; |
| | } |
| | |
| | .card::before { |
| | content: ''; |
| | position: absolute; |
| | top: 0; |
| | left: 0; |
| | right: 0; |
| | height: 4px; |
| | background: var(--gradient-primary); |
| | transform: scaleX(0); |
| | transform-origin: right; |
| | transition: transform 0.4s ease; |
| | } |
| | |
| | .card:hover::before { |
| | transform: scaleX(1); |
| | transform-origin: left; |
| | } |
| | |
| | .card:hover{ |
| | transform: translateY(-5px); |
| | box-shadow:0 15px 30px rgba(0,0,0,0.1); |
| | } |
| | |
| | /* بهبود تبها */ |
| | .tabs{ |
| | display:flex; |
| | gap:12px; |
| | justify-content:center; |
| | margin-bottom:24px; |
| | position:relative; |
| | padding:8px; |
| | background:linear-gradient(145deg, #f1f3f5, #e9ecef); |
| | border-radius:20px; |
| | box-shadow: inset 0 2px 4px rgba(0,0,0,0.06); |
| | flex-wrap: wrap; |
| | } |
| | |
| | .tab{ |
| | padding:14px 24px; |
| | border-radius:14px; |
| | background:transparent; |
| | color:var(--muted); |
| | cursor:pointer; |
| | font-weight:700; |
| | transition:var(--transition); |
| | position:relative; |
| | z-index:2; |
| | font-size: 1rem; |
| | white-space: nowrap; |
| | text-overflow: ellipsis; |
| | overflow: hidden; |
| | max-width: 200px; |
| | } |
| | |
| | .tab:hover{ |
| | color:var(--p); |
| | } |
| | |
| | .tab.active{ |
| | background:var(--card); |
| | color:var(--p); |
| | box-shadow:0 4px 12px rgba(0,0,0,0.08); |
| | transform: scale(1.05); |
| | } |
| | |
| | .tab-content{ |
| | display:none; |
| | overflow-x:hidden; |
| | animation: fadeIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); |
| | } |
| | |
| | .tab-content.active{ |
| | display:block; |
| | } |
| | |
| | @keyframes fadeIn { |
| | from { opacity: 0; transform: translateY(20px); } |
| | to { opacity: 1; transform: translateY(0); } |
| | } |
| | |
| | /* بهبود گرید */ |
| | .grid{ |
| | display:grid; |
| | grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); |
| | gap:20px; |
| | } |
| | |
| | /* بهبود لیبلها و اینپوتها */ |
| | label{ |
| | display:block; |
| | font-weight:700; |
| | color:var(--p-dark); |
| | margin-bottom:10px; |
| | font-size:0.95rem; |
| | letter-spacing: -0.01em; |
| | } |
| | |
| | input,select,textarea{ |
| | width:100%; |
| | padding:14px 18px; |
| | border-radius:12px; |
| | border:1px solid var(--border); |
| | background:var(--card); |
| | color:var(--text); |
| | font-size:1rem; |
| | transition:var(--transition); |
| | font-family:inherit; |
| | box-shadow: 0 2px 4px rgba(0,0,0,0.04); |
| | } |
| | |
| | input:focus,select:focus,textarea:focus{ |
| | outline:none; |
| | border-color:var(--p); |
| | box-shadow:0 0 0 4px var(--p-light), 0 4px 6px rgba(0,0,0,0.05); |
| | transform: translateY(-2px); |
| | } |
| | |
| | input[readonly]{ |
| | background:var(--bg); |
| | color:var(--text-light); |
| | } |
| | |
| | /* بهبود دکمهها */ |
| | button{ |
| | border:0; |
| | padding:12px 20px; |
| | border-radius:12px; |
| | background:var(--gradient-primary); |
| | color:#fff; |
| | font-weight:700; |
| | cursor:pointer; |
| | transition:var(--transition); |
| | font-size:1rem; |
| | display:inline-flex; |
| | align-items:center; |
| | justify-content:center; |
| | box-shadow:var(--shadow); |
| | position: relative; |
| | overflow: hidden; |
| | } |
| | |
| | button::before { |
| | content: ''; |
| | position: absolute; |
| | top: 0; |
| | left: 0; |
| | width: 100%; |
| | height: 100%; |
| | background: rgba(255,255,255,0.2); |
| | transform: translateX(-100%); |
| | transition: transform 0.6s ease; |
| | } |
| | |
| | button:hover::before { |
| | transform: translateX(0); |
| | } |
| | |
| | button:hover{ |
| | transform: translateY(-3px); |
| | box-shadow:var(--shadow-hover); |
| | } |
| | |
| | button:active{ |
| | transform: translateY(-1px); |
| | } |
| | |
| | button:disabled{ |
| | background:var(--muted); |
| | cursor:not-allowed; |
| | opacity:0.7; |
| | transform:none; |
| | box-shadow:none; |
| | } |
| | |
| | .btn-muted{ |
| | background:var(--gradient-secondary); |
| | } |
| | |
| | .btn-muted:hover{ |
| | background:var(--gradient-secondary); |
| | filter: brightness(1.1); |
| | } |
| | |
| | .btn-danger{ |
| | background:var(--gradient-danger); |
| | } |
| | |
| | .btn-danger:hover{ |
| | filter: brightness(1.1); |
| | } |
| | |
| | /* دکمه ویژه مدیر */ |
| | .btn-manager { |
| | background: var(--gradient-manager); |
| | border: 2px solid rgba(255, 255, 255, 0.3); |
| | box-shadow: 0 6px 12px rgba(30, 58, 138, 0.4); |
| | font-weight: 800; |
| | letter-spacing: 0.5px; |
| | position: relative; |
| | overflow: hidden; |
| | z-index: 1; |
| | } |
| | |
| | .btn-manager::before { |
| | content: ''; |
| | position: absolute; |
| | top: 0; |
| | left: 0; |
| | width: 100%; |
| | height: 100%; |
| | background: linear-gradient(45deg, rgba(255,255,255,0.1), rgba(255,255,255,0.3)); |
| | transform: translateX(-100%); |
| | transition: transform 0.8s ease; |
| | z-index: -1; |
| | } |
| | |
| | .btn-manager:hover::before { |
| | transform: translateX(0); |
| | } |
| | |
| | .btn-manager:hover { |
| | transform: translateY(-4px); |
| | box-shadow: 0 12px 24px rgba(30, 58, 138, 0.5); |
| | filter: brightness(1.1); |
| | } |
| | |
| | .btn-manager::after { |
| | content: '📋'; |
| | margin-left: 8px; |
| | font-size: 1.2rem; |
| | } |
| | |
| | /* بهبود جداول */ |
| | table{ |
| | width:100%; |
| | border-collapse:collapse; |
| | margin-top:20px; |
| | table-layout:fixed; |
| | font-size:0.95rem; |
| | border-radius: 12px; |
| | overflow: hidden; |
| | box-shadow: 0 4px 6px rgba(0,0,0,0.05); |
| | } |
| | |
| | th,td{ |
| | padding:14px 18px; |
| | border:1px solid var(--border); |
| | text-align:center; |
| | vertical-align:middle; |
| | word-break:break-word; |
| | white-space:normal; |
| | } |
| | |
| | th{ |
| | background:var(--gradient-primary); |
| | color:#fff; |
| | font-weight:700; |
| | text-shadow: 0 1px 2px rgba(0,0,0,0.1); |
| | } |
| | |
| | tbody tr:nth-child(even){ |
| | background:var(--bg); |
| | } |
| | |
| | tbody tr:hover{ |
| | background-color:rgba(67,97,238,0.08); |
| | transform: scale(1.01); |
| | transition: all 0.2s ease; |
| | } |
| | |
| | /* بهبود فوتر */ |
| | footer{ |
| | padding:24px; |
| | text-align:center; |
| | color:var(--text-light); |
| | font-weight:600; |
| | margin-top:40px; |
| | border-top:1px solid var(--border); |
| | background: rgba(255,255,255,0.7); |
| | backdrop-filter: blur(10px); |
| | border-radius: 20px; |
| | margin: 40px auto; |
| | max-width: 1200px; |
| | } |
| | |
| | /* بهبود نشان نویسنده - طراحی کاملاً جدید */ |
| | .author-badge{ |
| | position:fixed; |
| | left:24px; |
| | bottom:24px; |
| | background:var(--gradient-primary); |
| | padding:16px 24px; |
| | border-radius:50px; |
| | box-shadow:var(--shadow-card); |
| | font-weight:800; |
| | color:#fff; |
| | z-index:100; |
| | transition:var(--transition); |
| | display: flex; |
| | align-items: center; |
| | gap: 12px; |
| | border: 2px solid rgba(255,255,255,0.2); |
| | backdrop-filter: blur(10px); |
| | overflow: hidden; |
| | } |
| | |
| | .author-badge::before { |
| | content: ''; |
| | position: absolute; |
| | top: -50%; |
| | left: -50%; |
| | width: 200%; |
| | height: 200%; |
| | background: linear-gradient( |
| | 45deg, |
| | rgba(255,255,255,0) 0%, |
| | rgba(255,255,255,0.2) 50%, |
| | rgba(255,255,255,0) 100% |
| | ); |
| | transform: rotate(30deg); |
| | animation: shine 3s infinite; |
| | z-index: -1; |
| | } |
| | |
| | .author-badge::after { |
| | content: '✦'; |
| | font-size: 1.4rem; |
| | background: rgba(255,255,255,0.2); |
| | width: 36px; |
| | height: 36px; |
| | border-radius: 50%; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | box-shadow: 0 4px 8px rgba(0,0,0,0.1); |
| | } |
| | |
| | .author-badge:hover{ |
| | transform:translateY(-5px) scale(1.05); |
| | box-shadow:0 15px 30px rgba(67,97,238,0.3); |
| | } |
| | |
| | .author-badge-text { |
| | display: flex; |
| | flex-direction: column; |
| | line-height: 1.2; |
| | } |
| | |
| | .author-badge-name { |
| | font-size: 1.1rem; |
| | margin-bottom: 2px; |
| | } |
| | |
| | .author-badge-title { |
| | font-size: 0.85rem; |
| | opacity: 0.9; |
| | } |
| | |
| | @keyframes shine { |
| | 0% { |
| | transform: translateX(-100%) translateY(-100%) rotate(30deg); |
| | } |
| | 100% { |
| | transform: translateX(100%) translateY(100%) rotate(30deg); |
| | } |
| | } |
| | |
| | .small-muted{ |
| | color:var(--text-light); |
| | font-size:0.95rem; |
| | } |
| | |
| | .hint{ |
| | font-size:0.95rem; |
| | color:var(--p); |
| | margin-top:12px; |
| | background:var(--p-light); |
| | padding:16px; |
| | border-radius:12px; |
| | border-right:5px solid var(--p); |
| | box-shadow: 0 4px 6px rgba(0,0,0,0.05); |
| | } |
| | |
| | /* بهبود رسپانسیو */ |
| | @media(max-width:720px){ |
| | header{ |
| | flex-direction:column; |
| | gap:16px; |
| | text-align:center; |
| | padding: 20px 24px; |
| | } |
| | |
| | header h1 { |
| | font-size: 1.5rem; |
| | } |
| | |
| | .author-badge{ |
| | left:auto; |
| | right:20px; |
| | bottom: 20px; |
| | padding: 14px 20px; |
| | } |
| | |
| | .author-badge-name { |
| | font-size: 1rem; |
| | } |
| | |
| | .author-badge-title { |
| | font-size: 0.8rem; |
| | } |
| | |
| | .grid{ |
| | grid-template-columns:1fr; |
| | gap: 16px; |
| | } |
| | |
| | table{ |
| | font-size:0.85rem; |
| | } |
| | |
| | th,td{ |
| | padding:10px; |
| | } |
| | |
| | .card { |
| | padding: 20px; |
| | margin-bottom: 20px; |
| | } |
| | |
| | .tabs { |
| | gap: 8px; |
| | padding: 6px; |
| | } |
| | |
| | .tab { |
| | padding: 10px 16px; |
| | font-size: 0.9rem; |
| | max-width: 150px; |
| | } |
| | |
| | /* 🆕 اصلاحات واکنشگرایی برای تب آنالیز */ |
| | .analysis-grid-2-col { |
| | grid-template-columns: 1fr; /* کارتها در موبایل زیر هم قرار میگیرند */ |
| | } |
| | .bar-label { |
| | width: 90px; /* کاهش عرض لیبل نمودار برای جای بیشتر */ |
| | font-size: 0.85rem; |
| | } |
| | .bar { |
| | font-size: 0.85rem; |
| | padding-right: 8px; |
| | } |
| | .analysis-stat-card p { |
| | font-size: 2rem; /* کمی کوچکتر کردن فونت آمار اصلی در موبایل */ |
| | } |
| | } |
| | |
| | /* استایلهای جدید برای فرمهای چندگانه */ |
| | .form-container { |
| | margin-top: 28px; |
| | padding: 24px; |
| | border: 1px solid var(--border); |
| | border-radius:16px; |
| | background: linear-gradient(145deg, #f8f9fa, #e9ecef); |
| | transition: var(--transition); |
| | box-shadow: inset 0 2px 4px rgba(0,0,0,0.06); |
| | } |
| | |
| | .form-container:hover { |
| | box-shadow: 0 4px 12px rgba(0,0,0,0.08); |
| | } |
| | |
| | .form-header { |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | margin-bottom: 24px; |
| | padding-bottom: 16px; |
| | border-bottom: 1px dashed var(--border); |
| | } |
| | |
| | .form-actions { |
| | display: flex; |
| | gap: 12px; |
| | } |
| | |
| | .worker-block { |
| | border: 1px dashed var(--border); |
| | padding:20px; |
| | border-radius:16px; |
| | margin-bottom:20px; |
| | background: linear-gradient(145deg, #f8f9fa, #e9ecef); |
| | transition: var(--transition); |
| | box-shadow: inset 0 2px 4px rgba(0,0,0,0.06); |
| | } |
| | |
| | .worker-block:hover { |
| | border-color: var(--p); |
| | background: var(--p-light); |
| | transform: translateY(-3px); |
| | box-shadow: 0 6px 12px rgba(0,0,0,0.08); |
| | } |
| | |
| | .worker-block .grid { |
| | gap: 16px; |
| | margin-bottom: 16px; |
| | } |
| | |
| | .worker-block .grid > div { |
| | min-width: 200px; |
| | } |
| | |
| | /* استایلهای جدید برای منوی خروجی */ |
| | .export-menu { |
| | position: fixed; |
| | top: 0; |
| | left: 0; |
| | width: 100%; |
| | height: 100%; |
| | background-color: rgba(0, 0, 0, 0.7); |
| | z-index: 1000; |
| | display: none; |
| | justify-content: center; |
| | align-items: center; |
| | padding: 24px; |
| | box-sizing: border-box; |
| | overflow-y: auto; |
| | backdrop-filter: blur(8px); |
| | } |
| | |
| | .export-content { |
| | background-color: var(--card); |
| | border-radius:24px; |
| | padding: 36px; |
| | width: 100%; |
| | max-width: 640px; |
| | box-shadow: var(--shadow-modal); |
| | max-height: 90vh; |
| | overflow-y: auto; |
| | animation: slideUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); |
| | position: relative; |
| | margin: auto; |
| | border: 1px solid rgba(255,255,255,0.8); |
| | } |
| | |
| | @keyframes slideUp { |
| | from { opacity: 0; transform: translateY(40px); } |
| | to { opacity: 1; transform: translateY(0); } |
| | } |
| | |
| | .export-header { |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | margin-bottom: 32px; |
| | padding-bottom: 20px; |
| | border-bottom: 1px solid var(--border); |
| | } |
| | |
| | .export-header h3 { |
| | margin: 0; |
| | color: var(--p-dark); |
| | font-size: 1.7rem; |
| | font-weight: 800; |
| | } |
| | |
| | .export-close { |
| | background: none; |
| | border: none; |
| | font-size: 32px; |
| | cursor: pointer; |
| | color: var(--muted); |
| | padding: 0; |
| | width: 48px; |
| | height: 48px; |
| | display: flex; |
| | justify-content: center; |
| | align-items: center; |
| | border-radius: 50%; |
| | transition: var(--transition); |
| | } |
| | |
| | .export-close:hover { |
| | background-color: var(--bg); |
| | color: var(--text); |
| | transform: rotate(90deg); |
| | } |
| | |
| | .export-options { |
| | display: grid; |
| | grid-template-columns: 1fr; |
| | gap: 20px; |
| | } |
| | |
| | .export-option { |
| | padding: 24px; |
| | border: 1px solid var(--border); |
| | border-radius:16px; |
| | background-color: var(--bg); |
| | cursor: pointer; |
| | transition: var(--transition); |
| | text-align: right; |
| | position: relative; |
| | overflow: hidden; |
| | box-shadow: 0 4px 6px rgba(0,0,0,0.05); |
| | } |
| | |
| | .export-option::before { |
| | content: ''; |
| | position: absolute; |
| | top: 0; |
| | right: 0; |
| | width: 6px; |
| | height: 100%; |
| | background: var(--gradient-primary); |
| | transform: scaleY(0); |
| | transition: transform 0.4s ease; |
| | } |
| | |
| | .export-option:hover { |
| | background-color: var(--p-light); |
| | border-color: var(--p); |
| | transform: translateY(-5px); |
| | box-shadow: 0 10px 20px rgba(26,115,232,0.15); |
| | } |
| | |
| | .export-option:hover::before { |
| | transform: scaleY(1); |
| | } |
| | |
| | .export-option h4 { |
| | margin: 0 0 12px 0; |
| | font-size: 1.3rem; |
| | font-weight: 800; |
| | color: var(--p-dark); |
| | } |
| | |
| | .export-option p { |
| | margin: 0; |
| | font-size: 1rem; |
| | color: var(--text-light); |
| | line-height: 1.6; |
| | } |
| | |
| | /* استایلهای مودال اشتراک با مدیر */ |
| | .modal { |
| | display: none; |
| | position: fixed; |
| | z-index: 1000; |
| | left: 0; |
| | top: 0; |
| | width: 100%; |
| | height: 100%; |
| | background-color: rgba(0,0,0,0.7); |
| | justify-content: center; |
| | align-items: center; |
| | padding: 24px; |
| | box-sizing: border-box; |
| | overflow-y: auto; |
| | backdrop-filter: blur(8px); |
| | } |
| | |
| | .modal-content { |
| | background-color: var(--card); |
| | margin: auto; |
| | padding: 32px; |
| | border: none; |
| | width: 90%; |
| | max-width: 640px; |
| | border-radius:24px; |
| | box-shadow: var(--shadow-modal); |
| | max-height: 90vh; |
| | overflow-y: auto; |
| | position: relative; |
| | animation: slideUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); |
| | border: 1px solid rgba(255,255,255,0.8); |
| | } |
| | |
| | .modal-header { |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | margin-bottom: 24px; |
| | padding-bottom: 20px; |
| | border-bottom: 1px solid var(--border); |
| | } |
| | |
| | .modal-header h3 { |
| | margin: 0; |
| | color: var(--p-dark); |
| | font-size: 1.7rem; |
| | font-weight: 800; |
| | } |
| | |
| | .close { |
| | color: var(--muted); |
| | font-size: 32px; |
| | font-weight: bold; |
| | cursor: pointer; |
| | line-height: 1; |
| | transition: var(--transition); |
| | width: 48px; |
| | height: 48px; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | border-radius: 50%; |
| | } |
| | |
| | .close:hover, |
| | .close:focus { |
| | color: var(--text); |
| | background-color: var(--bg); |
| | transform: rotate(90deg); |
| | } |
| | |
| | .modal-body { |
| | margin-bottom: 28px; |
| | } |
| | |
| | /* استایلهای کادر توضیحات */ |
| | #inspectionComments { |
| | resize: vertical; |
| | min-height: 140px; |
| | transition: var(--transition); |
| | } |
| | |
| | /* استایلهای رسپانسیو */ |
| | @media (max-width: 768px) { |
| | .export-content { |
| | padding: 28px; |
| | max-width: 100%; |
| | width: 95%; |
| | } |
| | .export-header h3 { |
| | font-size: 1.5rem; |
| | } |
| | .export-option { |
| | padding: 20px; |
| | } |
| | .export-option h4 { |
| | font-size: 1.2rem; |
| | } |
| | .export-option p { |
| | font-size: 0.95rem; |
| | } |
| | .modal-content { |
| | width: 95%; |
| | padding: 24px; |
| | } |
| | } |
| | |
| | @media (max-width: 480px) { |
| | .export-content { |
| | padding: 24px; |
| | width: 95%; |
| | } |
| | .export-header { |
| | margin-bottom: 24px; |
| | padding-bottom: 16px; |
| | } |
| | .export-header h3 { |
| | font-size: 1.4rem; |
| | } |
| | .export-close { |
| | width: 40px; |
| | height: 40px; |
| | font-size: 28px; |
| | } |
| | .export-option { |
| | padding: 16px; |
| | } |
| | .export-option h4 { |
| | font-size: 1.1rem; |
| | margin-bottom: 8px; |
| | } |
| | .export-option p { |
| | font-size: 0.9rem; |
| | } |
| | .modal-content { |
| | width: 95%; |
| | padding: 20px; |
| | } |
| | .modal-header h3 { |
| | font-size: 1.4rem; |
| | } |
| | } |
| | |
| | /* استایلهای جدید برای جداول با ستونهای زیاد */ |
| | .table-container { |
| | overflow-x: auto; |
| | margin-top: 20px; |
| | border-radius:16px; |
| | box-shadow: 0 4px 12px rgba(0,0,0,0.08); |
| | } |
| | |
| | #managersTable { |
| | min-width: 900px; |
| | } |
| | |
| | #managersTable th:nth-child(3), |
| | #managersTable td:nth-child(3), |
| | #managersTable th:nth-child(5), |
| | #managersTable td:nth-child(5), |
| | #managersTable th:nth-child(6), |
| | #managersTable td:nth-child(6), |
| | #managersTable th:nth-child(7), |
| | #managersTable td:nth-child(7) { |
| | min-width: 120px; |
| | max-width: 150px; |
| | } |
| | |
| | #managersTable th:nth-child(8), |
| | #managersTable td:nth-child(8), |
| | #managersTable th:nth-child(9), |
| | #managersTable td:nth-child(9) { |
| | min-width: 100px; |
| | max-width: 120px; |
| | } |
| | |
| | /* استایلهای جدید برای جدول بازرسیها */ |
| | #inspectionsTable, #searchResultsTable { |
| | min-width: 800px; |
| | } |
| | |
| | #inspectionsTable th:nth-child(1), |
| | #inspectionsTable td:nth-child(1), |
| | #searchResultsTable th:nth-child(1), |
| | #searchResultsTable td:nth-child(1) { |
| | width: 50px; |
| | min-width: 50px; |
| | } |
| | |
| | #inspectionsTable th:nth-child(2), |
| | #inspectionsTable td:nth-child(2), |
| | #searchResultsTable th:nth-child(2), |
| | #searchResultsTable td:nth-child(2) { |
| | width: 100px; |
| | min-width: 100px; |
| | } |
| | |
| | #inspectionsTable th:nth-child(3), |
| | #inspectionsTable td:nth-child(3), |
| | #searchResultsTable th:nth-child(3), |
| | #searchResultsTable td:nth-child(3) { |
| | width: 120px; |
| | min-width: 120px; |
| | } |
| | |
| | #inspectionsTable th:nth-child(4), |
| | #inspectionsTable td:nth-child(4), |
| | #searchResultsTable th:nth-child(4), |
| | #searchResultsTable td:nth-child(4) { |
| | width: 150px; |
| | min-width: 150px; |
| | } |
| | |
| | #inspectionsTable th:nth-child(5), |
| | #inspectionsTable td:nth-child(5), |
| | #searchResultsTable th:nth-child(5), |
| | #searchResultsTable td:nth-child(5) { |
| | width: 80px; |
| | min-width: 80px; |
| | } |
| | |
| | #inspectionsTable th:nth-child(6), |
| | #inspectionsTable td:nth-child(6), |
| | #searchResultsTable th:nth-child(6), |
| | #searchResultsTable td:nth-child(6) { |
| | width: 150px; |
| | min-width: 150px; |
| | word-break: break-all; |
| | font-size: 0.85rem; |
| | } |
| | |
| | #inspectionsTable th:nth-child(7), |
| | #inspectionsTable td:nth-child(7), |
| | #searchResultsTable th:nth-child(7), |
| | #searchResultsTable td:nth-child(7) { |
| | width: 100px; |
| | min-width: 100px; |
| | } |
| | |
| | #inspectionsTable th:nth-child(8), |
| | #inspectionsTable td:nth-child(8), |
| | #searchResultsTable th:nth-child(8), |
| | #searchResultsTable td:nth-child(8) { |
| | width: 150px; |
| | min-width: 150px; |
| | } |
| | |
| | /* استایلهای جدید برای جدول بازرسها */ |
| | #inspectorsTable { |
| | min-width: 700px; |
| | } |
| | |
| | #inspectorsTable th:nth-child(1), |
| | #inspectorsTable td:nth-child(1) { |
| | width: 50px; |
| | min-width: 50px; |
| | } |
| | |
| | #inspectorsTable th:nth-child(2), |
| | #inspectorsTable td:nth-child(2) { |
| | width: 120px; |
| | min-width: 120px; |
| | } |
| | |
| | #inspectorsTable th:nth-child(3), |
| | #inspectorsTable td:nth-child(3) { |
| | width: 100px; |
| | min-width: 100px; |
| | } |
| | |
| | #inspectorsTable th:nth-child(4), |
| | #inspectorsTable td:nth-child(4) { |
| | width: 120px; |
| | min-width: 120px; |
| | } |
| | |
| | #inspectorsTable th:nth-child(5), |
| | #inspectorsTable td:nth-child(5) { |
| | width: 120px; |
| | min-width: 120px; |
| | } |
| | |
| | #inspectorsTable th:nth-child(6), |
| | #inspectorsTable td:nth-child(6) { |
| | width: 150px; |
| | min-width: 150px; |
| | } |
| | |
| | /* استایلهای جدید برای دکمههای عملیات */ |
| | .edit-inspection, .delete-inspection, .edit-inspector, .delete-inspector, .edit-manager, .delete-manager { |
| | padding: 8px 14px; |
| | margin: 0 4px; |
| | font-size: 0.9rem; |
| | white-space: nowrap; |
| | border-radius:10px; |
| | } |
| | |
| | /* استایلهای جدید برای پیامهای فرم */ |
| | #formMessage, #searchMessage { |
| | background-color: var(--p-light); |
| | color: var(--p-dark); |
| | border-radius:12px; |
| | padding: 16px; |
| | border-right: 5px solid var(--p); |
| | animation: fadeIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); |
| | box-shadow: 0 4px 6px rgba(0,0,0,0.05); |
| | } |
| | |
| | /* استایلهای جدید برای لیست کارگران */ |
| | .workers-list { |
| | grid-column: 1 / -1; |
| | border-radius:16px; |
| | overflow: hidden; |
| | } |
| | |
| | /* استایلهای جدید برای تاریخ آخرین بازرسی */ |
| | #lastInspectionDate { |
| | background-color: var(--bg); |
| | border-radius:12px; |
| | padding: 16px; |
| | border: 1px solid var(--border); |
| | font-weight: 600; |
| | box-shadow: inset 0 2px 4px rgba(0,0,0,0.06); |
| | } |
| | |
| | /* استایلهای جدید برای اطلاعات بازرس */ |
| | #inspectorInfo { |
| | background-color: var(--p-light); |
| | border-radius:12px; |
| | padding: 16px; |
| | border-right: 5px solid var(--p); |
| | font-weight: 600; |
| | box-shadow: 0 4px 6px rgba(0,0,0,0.05); |
| | } |
| | |
| | /* استایلهای جدید برای دکمههای اصلی */ |
| | #btnGetGPS, #btnManualGPS, #btnSubmitInspection, #btnReset, #exportBtn { |
| | position: relative; |
| | overflow: hidden; |
| | } |
| | |
| | #btnGetGPS::after, #btnManualGPS::after, #btnSubmitInspection::after, #btnReset::after, #exportBtn::after { |
| | content: ''; |
| | position: absolute; |
| | top: 50%; |
| | left: 50%; |
| | width: 5px; |
| | height: 5px; |
| | background: rgba(255, 255, 255, 0.5); |
| | opacity: 0; |
| | border-radius: 100%; |
| | transform: scale(1, 1) translate(-50%); |
| | transform-origin: 50% 50%; |
| | } |
| | |
| | #btnGetGPS:focus:not(:active)::after, #btnManualGPS:focus:not(:active)::after, #btnSubmitInspection:focus:not(:active)::after, #btnReset:focus:not(:active)::after, #exportBtn:focus:not(:active)::after { |
| | animation: ripple 1s ease-out; |
| | } |
| | |
| | @keyframes ripple { |
| | 0% { |
| | transform: scale(0, 0); |
| | opacity: 0.5; |
| | } |
| | 100% { |
| | transform: scale(100, 100); |
| | opacity: 0; |
| | } |
| | } |
| | |
| | /* استایلهای جدید برای دکمههای اضافه/بهروزرسانی */ |
| | #btnAddInspector, #btnAddManager { |
| | background: var(--gradient-success); |
| | } |
| | |
| | #btnAddInspector:hover, #btnAddManager:hover { |
| | filter: brightness(1.1); |
| | } |
| | |
| | /* استایلهای جدید برای دکمههای پاک کردن فرم */ |
| | #btnClearInspector, #btnClearManager { |
| | background: var(--gradient-warning); |
| | color: var(--text); |
| | } |
| | |
| | #btnClearInspector:hover, #btnClearManager:hover { |
| | filter: brightness(1.1); |
| | } |
| | |
| | /* استایلهای جدید برای دکمه ارسال اشتراک */ |
| | #btnSendShare { |
| | background: var(--gradient-success); |
| | } |
| | |
| | #btnSendShare:hover { |
| | filter: brightness(1.1); |
| | } |
| | |
| | /* استایلهای جدید برای دکمه انصراف اشتراک */ |
| | #btnCancelShare { |
| | background: var(--gradient-secondary); |
| | } |
| | |
| | #btnCancelShare:hover { |
| | filter: brightness(1.1); |
| | } |
| | |
| | /* استایلهای جدید برای انتخابگر اشتراک با مدیر */ |
| | #shareManager, #shareMethod { |
| | cursor: pointer; |
| | } |
| | |
| | /* استایلهای جدید برای متن اضافی در اشتراک */ |
| | #shareMessage { |
| | min-height: 120px; |
| | resize: vertical; |
| | } |
| | |
| | /* استایلهای جدید برای افکتهای ویژه */ |
| | .shine-effect { |
| | position: relative; |
| | overflow: hidden; |
| | } |
| | |
| | .shine-effect::before { |
| | content: ''; |
| | position: absolute; |
| | top: -50%; |
| | left: -50%; |
| | width: 200%; |
| | height: 200%; |
| | background: linear-gradient( |
| | to right, |
| | rgba(255, 255, 255, 0) 0%, |
| | rgba(255, 255, 255, 0.3) 50%, |
| | rgba(255, 255, 255, 0) 100% |
| | ); |
| | transform: rotate(30deg); |
| | animation: shine 3s infinite; |
| | } |
| | |
| | @keyframes shine { |
| | 0% { |
| | transform: translateX(-100%) translateY(-100%) rotate(30deg); |
| | } |
| | 100% { |
| | transform: translateX(100%) translateY(100%) rotate(30deg); |
| | } |
| | } |
| | |
| | /* استایلهای جدید برای افکتهای لودینگ */ |
| | .loading { |
| | position: relative; |
| | pointer-events: none; |
| | } |
| | |
| | .loading::after { |
| | content: ''; |
| | position: absolute; |
| | top: 0; |
| | left: 0; |
| | width: 100%; |
| | height: 100%; |
| | background: rgba(255, 255, 255, 0.7); |
| | z-index: 1; |
| | } |
| | |
| | .loading::before { |
| | content: ''; |
| | position: absolute; |
| | top: 50%; |
| | left: 50%; |
| | width: 30px; |
| | height: 30px; |
| | margin: -15px 0 0 -15px; |
| | border: 3px solid rgba(0, 0, 0, 0.1); |
| | border-radius: 50%; |
| | border-top-color: var(--p); |
| | animation: spin 1s ease-in-out infinite; |
| | z-index: 2; |
| | } |
| | |
| | @keyframes spin { |
| | to { transform: rotate(360deg); } |
| | } |
| | |
| | /* استایلهای جدید برای چینش دکمههای فرم بازرسی */ |
| | .action-buttons { |
| | display: flex; |
| | flex-wrap: wrap; |
| | gap: 12px; |
| | justify-content: space-between; |
| | align-items: center; |
| | margin-top: 24px; |
| | padding: 20px; |
| | background: linear-gradient(145deg, #f8f9fa, #e9ecef); |
| | border-radius: 16px; |
| | box-shadow: inset 0 2px 4px rgba(0,0,0,0.06); |
| | } |
| | |
| | .action-buttons .button-group { |
| | display: flex; |
| | flex-wrap: wrap; |
| | gap: 10px; |
| | } |
| | |
| | .action-buttons .button-group.primary { |
| | flex: 1; |
| | justify-content: flex-start; |
| | } |
| | |
| | .action-buttons .button-group.secondary { |
| | flex: 1; |
| | justify-content: flex-end; |
| | } |
| | |
| | .action-buttons button { |
| | flex: 1; |
| | min-width: 150px; |
| | } |
| | |
| | @media (max-width: 768px) { |
| | .action-buttons { |
| | flex-direction: column; |
| | gap: 16px; |
| | } |
| | |
| | .action-buttons .button-group { |
| | width: 100%; |
| | justify-content: center !important; |
| | } |
| | |
| | .action-buttons button { |
| | min-width: 120px; |
| | } |
| | } |
| | |
| | /* استایلهای جدید برای فیلدهای اختیاری */ |
| | .optional-field { |
| | position: relative; |
| | } |
| | |
| | .optional-field::after { |
| | content: '(اختیاری)'; |
| | position: absolute; |
| | left: 12px; |
| | top: 50%; |
| | transform: translateY(-50%); |
| | font-size: 0.75rem; |
| | color: var(--text-light); |
| | background: var(--card); |
| | padding: 2px 6px; |
| | border-radius: 4px; |
| | } |
| | |
| | /* 🎨 استایلهای جدید برای تب تحلیل */ |
| | .analysis-grid-2-col { |
| | display: grid; |
| | grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); |
| | gap: 24px; |
| | } |
| | .analysis-stat-card { |
| | background: var(--card); |
| | border-radius: 16px; |
| | padding: 20px; |
| | text-align: center; |
| | border-left: 5px solid var(--stat-color, var(--p)); |
| | box-shadow: var(--shadow); |
| | transition: var(--transition); |
| | } |
| | .analysis-stat-card:hover { |
| | transform: translateY(-4px); |
| | box-shadow: var(--shadow-hover); |
| | } |
| | .analysis-stat-card h4 { |
| | margin: 0 0 10px 0; |
| | font-size: 1rem; |
| | color: var(--text-light); |
| | font-weight: 700; |
| | } |
| | .analysis-stat-card p { |
| | margin: 0; |
| | font-size: 2.5rem; |
| | font-weight: 800; |
| | color: var(--stat-color, var(--p-dark)); |
| | } |
| | .chart-container { |
| | min-height: 100px; |
| | width: 100%; |
| | } |
| | .bar-chart { |
| | display: flex; |
| | flex-direction: column; |
| | gap: 12px; |
| | } |
| | .bar-item { |
| | display: flex; |
| | align-items: center; |
| | gap: 12px; |
| | } |
| | .bar-label { |
| | width: 120px; |
| | text-align: right; |
| | font-weight: 700; |
| | font-size: 0.9rem; |
| | white-space: nowrap; |
| | overflow: hidden; |
| | text-overflow: ellipsis; |
| | color: var(--text); |
| | flex-shrink: 0; |
| | } |
| | .bar-wrapper { |
| | flex-grow: 1; |
| | background: var(--bg); |
| | border-radius: 8px; |
| | overflow: hidden; |
| | box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); |
| | } |
| | .bar { |
| | height: 28px; |
| | background: var(--gradient-primary); |
| | border-radius: 8px; |
| | color: white; |
| | display: flex; |
| | align-items: center; |
| | justify-content: flex-end; |
| | padding-right: 12px; |
| | font-weight: 700; |
| | font-size: 0.9rem; |
| | transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); |
| | white-space: nowrap; |
| | } |
| | #btnShowMap { |
| | width: 100%; |
| | padding: 16px; |
| | font-size: 1.1rem; |
| | background: var(--gradient-success); |
| | } |
| | #btnShowMap:hover { |
| | filter: brightness(1.1); |
| | } |
| | .card-header-icon { |
| | margin-left: 8px; |
| | vertical-align: middle; |
| | } |
| | /* استایلهای جدید برای پیام مدرن */ |
| | .modern-alert { |
| | position: fixed; |
| | top: 50%; |
| | left: 50%; |
| | transform: translate(-50%, -50%) scale(0.9); |
| | background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(240, 240, 240, 0.9)); |
| | backdrop-filter: blur(10px); |
| | color: var(--p-dark); |
| | padding: 24px 32px; |
| | border-radius: 20px; |
| | box-shadow: var(--shadow-modal); |
| | z-index: 2000; |
| | text-align: center; |
| | font-size: 1.1rem; |
| | font-weight: 700; |
| | border: 1px solid rgba(255, 255, 255, 1); |
| | opacity: 0; |
| | transition: opacity 0.5s ease, transform 0.5s ease; |
| | display: flex; |
| | flex-direction: column; |
| | align-items: center; |
| | gap: 16px; |
| | max-width: 90%; |
| | pointer-events: none; |
| | } |
| | .modern-alert.show { |
| | opacity: 1; |
| | transform: translate(-50%, -50%) scale(1); |
| | } |
| | .modern-alert .spinner { |
| | width: 40px; |
| | height: 40px; |
| | border: 4px solid var(--p-light); |
| | border-top-color: var(--p); |
| | border-radius: 50%; |
| | animation: spin 1s linear infinite; |
| | } |
| | |
| | /* 🌟 استایلهای جدید برای تب تنظیمات و راهنما */ |
| | .settings-grid { |
| | display: grid; |
| | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
| | gap: 20px; |
| | } |
| | .setting-card { |
| | background: linear-gradient(145deg, var(--bg), #fff); |
| | border-radius: 16px; |
| | padding: 20px; |
| | border: 1px solid var(--border); |
| | text-align: center; |
| | transition: var(--transition); |
| | } |
| | .setting-card:hover { |
| | transform: translateY(-4px); |
| | box-shadow: var(--shadow-hover); |
| | border-color: var(--p); |
| | } |
| | .setting-card button { |
| | width: 100%; |
| | } |
| | .setting-card p { |
| | font-size: 0.9rem; |
| | color: var(--text-light); |
| | margin: 12px 0; |
| | min-height: 50px; |
| | } |
| | .help-section details { |
| | background: var(--bg); |
| | border-radius: 12px; |
| | margin-bottom: 12px; |
| | border: 1px solid var(--border); |
| | transition: var(--transition); |
| | } |
| | .help-section details:hover { |
| | border-color: var(--p); |
| | } |
| | .help-section summary { |
| | padding: 16px; |
| | font-weight: 700; |
| | color: var(--p-dark); |
| | cursor: pointer; |
| | list-style: none; |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | } |
| | .help-section summary::-webkit-details-marker { |
| | display: none; |
| | } |
| | .help-section summary::after { |
| | content: '▼'; |
| | transition: transform 0.3s; |
| | } |
| | .help-section details[open] summary::after { |
| | transform: rotate(180deg); |
| | } |
| | .help-section .details-content { |
| | padding: 0 16px 16px 16px; |
| | border-top: 1px solid var(--border); |
| | line-height: 1.7; |
| | color: var(--text); |
| | } |
| | .details-content p { |
| | margin-bottom: 1em; |
| | } |
| | .details-content code { |
| | background-color: var(--p-light); |
| | padding: 2px 6px; |
| | border-radius: 4px; |
| | color: var(--p-dark); |
| | font-family: monospace; |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <header> |
| | <h1>سامانه بازرسی و پلاک برداری از کارگاه ها — سازمان تامین اجتماعی</h1> |
| | <div class="author"> |
| | طراح و توسعه دهنده: <strong>اشکان پورعلی</strong><br> |
| | حسابدار واحد مالی سازمان تامین اجتماعی — شعبه دو اهواز |
| | <button id="btnSaveData" class="btn-muted shine-effect">ذخیره دادهها</button> |
| | </div> |
| | </header> |
| | <main> |
| | <div class="card"> |
| | <div class="tabs" role="tablist"> |
| | <div class="tab active" data-target="inspectionTab" role="tab" tabindex="0">فرم بازرسی</div> |
| | <div class="tab" data-target="inspectorsTab" role="tab" tabindex="-1">ثبت بازرسها</div> |
| | <div class="tab" data-target="managersTab" role="tab" tabindex="-1">ثبت مدیران </div> |
| | <div class="tab" data-target="analysisTab" role="tab" tabindex="-1">📊 انالیز دادهها</div> |
| | <div class="tab" data-target="searchTab" role="tab" tabindex="-1">🔎 جستجوی پیشرفته</div> |
| | <div class="tab" data-target="settingsTab" role="tab" tabindex="-1">⚙️ تنظیمات و راهنما</div> |
| | </div> |
| | <section id="inspectionTab" class="tab-content active" role="tabpanel"> |
| | <div class="card"> |
| | <form id="inspectionForm" class="grid" autocomplete="off" novalidate> |
| | <div> |
| | <label for="insp_personnel">شماره پرسنلی بازرس</label> |
| | <input id="insp_personnel" type="text" /> |
| | </div> |
| | <div style="grid-column:1/-1"> |
| | <div id="inspectorInfo" class="small-muted"></div> |
| | </div> |
| | <div><label for="workshopName">نام کارگاه</label><input id="workshopName" type="text" /></div> |
| | <div><label for="activityField">رسته فعالیت</label><input id="activityField" type="text" /></div> |
| | <div><label for="employerName">نام کارفرما</label><input id="employerName" type="text" /></div> |
| | <div><label for="employerNationalId">کد ملی کارفرما</label><input id="employerNationalId" type="text" /></div> |
| | <div><label for="numEmployees">تعداد نیروهای شاغل</label><input id="numEmployees" type="number" min="0" max="200" value="0" /></div> |
| | <div class="workers-list" id="workersList"></div> |
| | <div style="grid-column:1/-1"> |
| | <label>تاریخ ثبت آخرین بازرسی (شمسی — خودکار)</label> |
| | <div id="lastInspectionDate" class="small-muted">هنوز بازرسی ثبت نشده</div> |
| | </div> |
| | <div><label for="gpsField">مختصات GPS</label><input id="gpsField" type="text" readonly placeholder="lat, lon" /></div> |
| | <div style="grid-column:1/-1"> |
| | <label for="inspectionComments">توضیحات و یادداشتهای بازرسی</label> |
| | <textarea id="inspectionComments" rows="4" placeholder="توضیحات و یادداشتهای خود را در مورد این بازرسی وارد کنید..."></textarea> |
| | </div> |
| | <div style="grid-column:1/-1"> |
| | <div class="action-buttons"> |
| | <div class="button-group primary"> |
| | <button id="btnGetGPS" type="button">ثبت مختصات GPS</button> |
| | <button id="btnManualGPS" type="button" class="btn-muted">وارد کردن دستی مختصات</button> |
| | </div> |
| | <div class="button-group secondary"> |
| | <button id="btnSubmitInspection" type="button" disabled>ثبت بازرسی</button> |
| | <button id="btnSendToManager" type="button" class="btn-manager" disabled>ارسال مستقیم به رئیس شعبه</button> |
| | <button id="exportBtn" type="button" class="btn-muted" disabled>خروجی گزارش</button> |
| | <button id="btnReset" type="button" class="btn-muted">پاک کردن فرم</button> |
| | </div> |
| | </div> |
| | </div> |
| | <div id="formMessage" class="small-muted" style="grid-column:1/-1;display:none"></div> |
| | <div style="grid-column:1/-1"> |
| | <div class="hint">تذکر: اگر فایل را بهصورت محلی (از حافظهٔ گوشی) اجرا میکنید و دکمهٔ GPS عمل نکرد، ابتدا تلاش میکنیم مختصات تقریبی از طریق IP بهدست آوریم. اگر دستگاه آفلاین یا API پاسخگو نبود، میتوانید مختصات را بهصورت دستی وارد کنید.</div> |
| | </div> |
| | </form> |
| | </div> |
| | <div class="card"> |
| | <h3 style="margin:0 0 16px 0;color:var(--p-dark)">لیست بازرسیهای ثبتشده</h3> |
| | <div class="table-container"> |
| | <table id="inspectionsTable" aria-label="لیست بازرسیها"> |
| | <thead> |
| | <tr><th>#</th><th>شماره پرسنلی</th><th>بازرس</th><th>نام کارگاه</th><th>تعداد نیرو</th><th>مختصات (Plus Code)</th><th>تاریخ شمسی</th><th>عملیات</th></tr> |
| | </thead> |
| | <tbody></tbody> |
| | </table> |
| | </div> |
| | </div> |
| | </section> |
| | <section id="inspectorsTab" class="tab-content" role="tabpanel" aria-hidden="true"> |
| | <div class="card"> |
| | <form id="inspectorForm" class="grid" autocomplete="off" novalidate> |
| | <div><label for="mgr_national">کد ملی بازرس</label><input id="mgr_national" type="text" /></div> |
| | <div><label for="mgr_name">نام</label><input id="mgr_name" type="text" /></div> |
| | <div><label for="mgr_family">نام خانوادگی</label><input id="mgr_family" type="text" /></div> |
| | <div><label for="mgr_personnel">شماره پرسنلی</label><input id="mgr_personnel" type="text" /></div> |
| | <div style="grid-column:1/-1" class="buttons"> |
| | <button id="btnAddInspector" type="button">افزودن/بهروزرسانی بازرس</button> |
| | <button id="btnClearInspector" type="button" class="btn-muted">پاک کردن فرم</button> |
| | </div> |
| | </form> |
| | </div> |
| | <div class="card"> |
| | <h3 style="margin:0 0 16px 0;color:var(--p-dark)">لیست بازرسها</h3> |
| | <div class="table-container"> |
| | <table id="inspectorsTable" aria-label="لیست بازرسها"> |
| | <thead><tr><th>#</th><th>کد ملی</th><th>نام</th><th>نام خانوادگی</th><th>شماره پرسنلی</th><th>عملیات</th></tr></thead> |
| | <tbody></tbody> |
| | </table> |
| | </div> |
| | </div> |
| | </section> |
| | <section id="managersTab" class="tab-content" role="tabpanel" aria-hidden="true"> |
| | <div class="card"> |
| | <form id="managerForm" class="grid" autocomplete="off" novalidate> |
| | <div><label for="mgt_national">کد ملی مدیر</label><input id="mgt_national" type="text" /></div> |
| | <div><label for="mgt_name">نام</label><input id="mgt_name" type="text" /></div> |
| | <div><label for="mgt_family">نام خانوادگی</label><input id="mgt_family" type="text" /></div> |
| | <div><label for="mgt_position">سمت</label><input id="mgt_position" type="text" /></div> |
| | <div><label for="mgt_email">ایمیل</label><input id="mgt_email" type="email" /></div> |
| | <div><label for="mgt_phone">تلفن تماس</label><input id="mgt_phone" type="tel" /></div> |
| | <div><label for="mgt_telegram">نام کاربری تلگرام</label><input id="mgt_telegram" type="text" placeholder="بدون @" /></div> |
| | <div style="grid-column:1/-1" class="buttons"> |
| | <button id="btnAddManager" type="button">افزودن/بهروزرسانی مدیر</button> |
| | <button id="btnClearManager" type="button" class="btn-muted">پاک کردن فرم</button> |
| | </div> |
| | </form> |
| | </div> |
| | <div class="card"> |
| | <h3 style="margin:0 0 16px 0;color:var(--p-dark)">لیست مدیران</h3> |
| | <div class="table-container"> |
| | <table id="managersTable" aria-label="لیست مدیران"> |
| | <thead><tr><th>#</th><th>کد ملی</th><th>نام</th><th>نام خانوادگی</th><th>سمت</th><th>ایمیل</th><th>تلفن</th><th>تلگرام</th><th>عملیات</th></tr></thead> |
| | <tbody></tbody> |
| | </table> |
| | </div> |
| | </div> |
| | </section> |
| | <section id="analysisTab" class="tab-content" role="tabpanel" aria-hidden="true"> |
| | <div class="card"> |
| | <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;"> |
| | <h3 style="margin:0; color:var(--p-dark)"> |
| | <span class="card-header-icon">🚀</span>داشبورد هوشمند و آنالیز داده ها |
| | </h3> |
| | <button id="btnRefreshAnalysis" type="button" class="btn-muted">بروزرسانی آمار</button> |
| | </div> |
| | <div class="grid" id="analysis-grid"> |
| | <div class="analysis-stat-card" style="--stat-color: var(--p);"> |
| | <h4>کل بازرسیها</h4> |
| | <p id="totalInspectionsStat">0</p> |
| | </div> |
| | <div class="analysis-stat-card" style="--stat-color: var(--s);"> |
| | <h4>تعداد بازرس فعال</h4> |
| | <p id="totalInspectorsStat">0</p> |
| | </div> |
| | <div class="analysis-stat-card" style="--stat-color: var(--y);"> |
| | <h4>میانگین نیرو در کارگاه</h4> |
| | <p id="avgEmployeesStat">0</p> |
| | </div> |
| | <div class="analysis-stat-card" style="--stat-color: var(--w);"> |
| | <h4>کل نیروهای ثبت شده</h4> |
| | <p id="totalWorkersStat">0</p> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <div class="analysis-grid-2-col"> |
| | <div class="card"> |
| | <h3 style="margin:0 0 16px 0;color:var(--p-dark)"> |
| | <span class="card-header-icon">📈</span>تفکیک بازرسی بر اساس رسته فعالیت |
| | </h3> |
| | <div id="activityFieldChart" class="chart-container"> |
| | <p class="small-muted">دادهای برای نمایش وجود ندارد.</p> |
| | </div> |
| | </div> |
| | <div class="card"> |
| | <h3 style="margin:0 0 16px 0;color:var(--p-dark)"> |
| | <span class="card-header-icon">📊</span>روند بازرسیها در طول زمان |
| | </h3> |
| | <div id="inspectionsOverTimeChart" class="chart-container"> |
| | <p class="small-muted">دادهای برای نمایش وجود ندارد.</p> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <div class="card"> |
| | <h3 style="margin:0 0 16px 0;color:var(--p-dark)"> |
| | <span class="card-header-icon">👥</span>آنالیز پیشرفته عملکرد بازرسان |
| | </h3> |
| | <div id="advancedInspectorStats" class="table-container"> |
| | <p class="small-muted" style="text-align:center; padding: 20px;">دادهای برای نمایش وجود ندارد.</p> |
| | </div> |
| | </div> |
| |
|
| | <div class="analysis-grid-2-col"> |
| | <div class="card"> |
| | <h3 style="margin:0 0 16px 0;color:var(--p-dark)"> |
| | <span class="card-header-icon">🏢</span>توزیع کارگاهها بر اساس تعداد نیرو |
| | </h3> |
| | <div id="employeeDistributionChart" class="chart-container"> |
| | <p class="small-muted">دادهای برای نمایش وجود ندارد.</p> |
| | </div> |
| | </div> |
| | <div class="card"> |
| | <h3 style="margin:0 0 16px 0;color:var(--p-dark)"> |
| | <span class="card-header-icon">🗺️</span>نمایش پراکندگی جغرافیایی |
| | </h3> |
| | <p class="small-muted" style="margin-bottom: 20px;"> |
| | نقاط ثبت شده تمام بازرسیها را به صورت همزمان بر روی نقشه گوگل مشاهده کنید. |
| | </p> |
| | <button id="btnShowMap" type="button"> |
| | 📍 نمایش همه نقاط روی نقشه |
| | </button> |
| | </div> |
| | </div> |
| | </section> |
| | |
| | <section id="searchTab" class="tab-content" role="tabpanel" aria-hidden="true"> |
| | <div class="card"> |
| | <h3 style="margin:0 0 24px 0; color:var(--p-dark)"> |
| | <span class="card-header-icon">🔎</span>فیلتر و جستجوی پیشرفته بازرسیها |
| | </h3> |
| | <form id="searchForm" class="grid" autocomplete="off" novalidate> |
| | <div> |
| | <label for="searchInspector">بازرس</label> |
| | <select id="searchInspector"> |
| | <option value="">همه بازرسها</option> |
| | </select> |
| | </div> |
| | <div> |
| | <label for="searchWorkshopName">نام کارگاه</label> |
| | <input type="text" id="searchWorkshopName" placeholder="بخشی از نام کارگاه..."> |
| | </div> |
| | <div> |
| | <label for="searchActivityField">رسته فعالیت</label> |
| | <input type="text" id="searchActivityField" placeholder="بخشی از رسته فعالیت..."> |
| | </div> |
| | <div> |
| | <label for="searchEmployerName">نام کارفرما</label> |
| | <input type="text" id="searchEmployerName" placeholder="بخشی از نام کارفرما..."> |
| | </div> |
| | <div> |
| | <label for="searchMinEmployees">حداقل تعداد نیرو</label> |
| | <input type="number" id="searchMinEmployees" min="0" placeholder="مثلا: 5"> |
| | </div> |
| | <div> |
| | <label for="searchMaxEmployees">حداکثر تعداد نیرو</label> |
| | <input type="number" id="searchMaxEmployees" min="0" placeholder="مثلا: 20"> |
| | </div> |
| | <div style="grid-column:1/-1" class="action-buttons" > |
| | <div class="button-group primary"> |
| | <button id="btnRunSearch" type="button">اعمال فیلتر و جستجو</button> |
| | <button id="btnClearSearch" type="button" class="btn-muted">پاک کردن فیلترها</button> |
| | </div> |
| | <div class="button-group secondary"> |
| | <button id="btnExportFiltered" type="button" class="btn-manager" disabled>خروجی نتایج فیلتر شده</button> |
| | </div> |
| | </div> |
| | <div id="searchMessage" class="small-muted" style="grid-column:1/-1;display:none"></div> |
| | </form> |
| | </div> |
| | <div class="card"> |
| | <h3 style="margin:0 0 16px 0;color:var(--p-dark)">نتایج جستجو</h3> |
| | <p id="searchResultSummary" class="small-muted">برای مشاهده نتایج، فیلترها را تنظیم کرده و روی دکمه "اعمال فیلتر" کلیک کنید.</p> |
| | <div class="table-container"> |
| | <table id="searchResultsTable" aria-label="نتایج جستجوی بازرسیها"> |
| | <thead> |
| | <tr><th>#</th><th>شماره پرسنلی</th><th>بازرس</th><th>نام کارگاه</th><th>تعداد نیرو</th><th>مختصات (Plus Code)</th><th>تاریخ شمسی</th><th>عملیات</th></tr> |
| | </thead> |
| | <tbody></tbody> |
| | </table> |
| | </div> |
| | </div> |
| | </section> |
| |
|
| | <section id="settingsTab" class="tab-content" role="tabpanel" aria-hidden="true"> |
| | <div class="card"> |
| | <h3 style="margin:0 0 24px 0; color:var(--p-dark)"> |
| | <span class="card-header-icon">⚙️</span>تنظیمات و مدیریت داده |
| | </h3> |
| | <div class="settings-grid"> |
| | <div class="setting-card"> |
| | <h4>ورود داده (Import)</h4> |
| | <p>بازگردانی اطلاعات از فایل پشتیبان (با فرمت JSON) که قبلاً ذخیره کردهاید.</p> |
| | <button id="btnImportData" type="button" class="btn-muted">انتخاب فایل پشتیبان</button> |
| | <input type="file" id="fileImporter" accept=".json" style="display:none;"> |
| | </div> |
| | <div class="setting-card"> |
| | <h4>خروج کلی داده (Export)</h4> |
| | <p>ذخیره یک فایل پشتیبان (JSON) از تمام بازرسیها، بازرسان و مدیران ثبت شده در سیستم.</p> |
| | <button id="btnExportAllData" type="button">ذخیره فایل پشتیبان</button> |
| | </div> |
| | <div class="setting-card"> |
| | <h4>پاکسازی کلیه دادهها</h4> |
| | <p>تمام اطلاعات ثبت شده (بازرسیها، بازرسان و مدیران) برای همیشه حذف خواهند شد.</p> |
| | <button id="btnClearAllData" type="button" class="btn-danger">حذف تمام اطلاعات</button> |
| | </div> |
| | </div> |
| | </div> |
| | <div class="card"> |
| | <h3 style="margin:0 0 24px 0; color:var(--p-dark)"> |
| | <span class="card-header-icon">📖</span>راهنمای استفاده از سامانه |
| | </h3> |
| | <div class="help-section"> |
| | <details> |
| | <summary>شروع به کار و ثبت اطلاعات اولیه</summary> |
| | <div class="details-content"> |
| | <p>برای شروع، ابتدا باید اطلاعات اولیه را در سیستم ثبت کنید:</p> |
| | <ol> |
| | <li><strong>ثبت بازرسها:</strong> به تب «ثبت بازرسها» بروید و اطلاعات تمام بازرسان از جمله کد ملی، نام و شماره پرسنلی را وارد کنید.</li> |
| | <li><strong>ثبت مدیران:</strong> به تب «ثبت مدیران» مراجعه کرده و اطلاعات مدیرانی که گزارشها برایشان ارسال میشود را ثبت نمایید. این اطلاعات برای استفاده از دکمه «ارسال مستقیم به رئیس شعبه» ضروری است.</li> |
| | </ol> |
| | </div> |
| | </details> |
| | <details> |
| | <summary>نحوه ثبت یک بازرسی جدید</summary> |
| | <div class="details-content"> |
| | <p>در تب اصلی «فرم بازرسی» مراحل زیر را دنبال کنید:</p> |
| | <ol> |
| | <li>شماره پرسنلی بازرس را وارد کنید. نام او باید به صورت خودکار نمایش داده شود.</li> |
| | <li>اطلاعات کارگاه، کارفرما و تعداد نیروهای شاغل را وارد کنید.</li> |
| | <li>اگر کارگری در کارگاه حضور دارد، فرم مربوط به اطلاعات آنها را پر کنید.</li> |
| | <li>برای ثبت موقعیت جغرافیایی، روی دکمه <strong>«ثبت مختصات GPS»</strong> کلیک کنید. اگر GPS در دسترس نبود، سیستم از مختصات مبتنی بر اینترنت استفاده میکند. در غیر این صورت، با دکمه «وارد کردن دستی» مختصات را وارد نمایید.</li> |
| | <li>پس از تکمیل فرم، روی دکمه <strong>«ثبت بازرسی»</strong> کلیک کنید.</li> |
| | </ol> |
| | </div> |
| | </details> |
| | <details> |
| | <summary>خروجی و اشتراکگذاری گزارشها</summary> |
| | <div class="details-content"> |
| | <p>پس از ثبت بازرسیها، میتوانید از آنها گزارش تهیه کنید:</p> |
| | <ul> |
| | <li><strong>دکمه خروجی گزارش:</strong> با این دکمه میتوانید از کل بازرسیها در فرمتهای مختلف مانند Excel, Word, PDF و متن ساده خروجی بگیرید.</li> |
| | <li><strong>ارسال مستقیم به رئیس شعبه:</strong> این دکمه یک مودال باز میکند که در آن میتوانید مدیر و روش ارسال (ایمیل، تلگرام و...) را انتخاب کنید. پس از تایید، متن گزارش در حافظه کلیپبورد شما کپی شده و اپلیکیشن مربوطه باز میشود تا شما متن را Paste و ارسال کنید.</li> |
| | </ul> |
| | </div> |
| | </details> |
| | <details> |
| | <summary>استفاده از جستجوی پیشرفته</summary> |
| | <div class="details-content"> |
| | <p>در تب «جستجوی پیشرفته» میتوانید بازرسیها را بر اساس معیارهای مختلف فیلتر کنید. پس از اعمال فیلتر، لیست نتایج در جدول پایین نمایش داده میشود. دکمه <strong>«خروجی نتایج فیلتر شده»</strong> به شما این امکان را میدهد که فقط از نتایجی که مشاهده میکنید، خروجی تهیه کنید.</p> |
| | </div> |
| | </details> |
| | </div> |
| | </div> |
| | </section> |
| | </div> |
| | </main> |
| | <div id="exportMenu" class="export-menu"> |
| | <div class="export-content"> |
| | <div class="export-header"> |
| | <h3>انتخاب فرمت خروجی</h3> |
| | <button id="exportClose" class="export-close">×</button> |
| | </div> |
| | <div class="export-options"> |
| | <div class="export-option" data-format="excel"> |
| | <h4>Excel (.xlsx)</h4> |
| | <p>خروجی اکسل با تمام جزئیات بازرسیها و امکان فیلتر و جستجو</p> |
| | </div> |
| | <div class="export-option" data-format="word"> |
| | <h4>Word (HTML)</h4> |
| | <p>فایل HTML قابل باز شدن در Microsoft Word با قالببندی حرفهای</p> |
| | </div> |
| | <div class="export-option" data-format="pdf"> |
| | <h4>PDF (چاپی)</h4> |
| | <p>باز کردن گزارش در پنجره جدید با قابلیت چاپ مستقیم یا ذخیره به صورت PDF</p> |
| | </div> |
| | <div class="export-option" data-format="text"> |
| | <h4>متن ساده (.txt)</h4> |
| | <p>فایل متنی با تمام اطلاعات بازرسیها مناسب برای ارسال از طریق ایمیل</p> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | <div id="shareModal" class="modal"> |
| | <div class="modal-content"> |
| | <div class="modal-header"> |
| | <h3>ارسال گزارش به مدیر</h3> |
| | <span class="close">×</span> |
| | </div> |
| | <div class="modal-body"> |
| | <div class="grid"> |
| | <div> |
| | <label for="shareManager">انتخاب مدیر</label> |
| | <select id="shareManager"> |
| | <option value="">-- انتخاب کنید --</option> |
| | </select> |
| | </div> |
| | <div> |
| | <label for="shareMethod">روش ارسال</label> |
| | <select id="shareMethod"> |
| | <option value="email">ایمیل</option> |
| | <option value="telegram">تلگرام</option> |
| | <option value="whatsapp">واتساپ</option> |
| | </select> |
| | </div> |
| | <div style="grid-column:1/-1"> |
| | <label for="shareMessage">پیام بازرس</label> |
| | <textarea id="shareMessage" rows="3" placeholder="پیام خود را وارد کنید..."></textarea> |
| | </div> |
| | </div> |
| | <div class="buttons"> |
| | <button id="btnSendShare" type="button" disabled>ارسال</button> |
| | <button id="btnCancelShare" type="button" class="btn-muted">انصراف</button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | <footer> |
| | © طراح و توسعه دهنده: اشکان پورعلی — حسابدار واحد مالی شعبه دو اهواز، — سازمان تامین اجتماعی |
| | </footer> |
| | <div class="author-badge"> |
| | <div class="author-badge-text"> |
| | <div class="author-badge-name">اشکان پورعلی</div> |
| | <div class="author-badge-title">حسابدار واحد مالی سازمان تامین اجتماعی شعبه دو اهواز</div> |
| | </div> |
| | </div> |
| | <script src="https://cdn.jsdelivr.net/npm/jalaali-js@1.1.3/dist/jalaali.min.js"></script> |
| | <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script> |
| | <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> |
| | <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> |
| | <script src="https://cdn.jsdelivr.net/npm/openlocationcode@1.0.3/openlocationcode.js"></script> |
| | <script> |
| | document.addEventListener('DOMContentLoaded', () => { |
| | |
| | document.querySelectorAll('form').forEach(form => { |
| | form.addEventListener('submit', (e) => { |
| | e.preventDefault(); |
| | return false; |
| | }); |
| | }); |
| | |
| | |
| | const tabs = document.querySelectorAll('.tab'); |
| | const tabContents = document.querySelectorAll('.tab-content'); |
| | function activateTab(targetId){ |
| | tabs.forEach(t => t.classList.toggle('active', t.dataset.target === targetId)); |
| | tabContents.forEach(c => { |
| | c.classList.toggle('active', c.id === targetId); |
| | c.setAttribute('aria-hidden', c.id === targetId ? 'false' : 'true'); |
| | }); |
| | |
| | if (targetId === 'analysisTab') { |
| | renderAnalysis(); |
| | } |
| | |
| | if (targetId === 'searchTab') { |
| | updateSearchInspectorOptions(); |
| | } |
| | } |
| | tabs.forEach(t => { |
| | t.addEventListener('click', () => activateTab(t.dataset.target)); |
| | t.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') activateTab(t.dataset.target); }); |
| | }); |
| | |
| | const KEY_INSPECTORS = 'inspectors'; |
| | const KEY_INSPECTIONS = 'inspections'; |
| | const KEY_MANAGERS = 'managers'; |
| | function loadInspectors(){ return JSON.parse(localStorage.getItem(KEY_INSPECTORS) || '[]'); } |
| | function saveInspectors(arr){ localStorage.setItem(KEY_INSPECTORS, JSON.stringify(arr)); } |
| | function loadInspections(){ return JSON.parse(localStorage.getItem(KEY_INSPECTIONS) || '[]'); } |
| | function saveInspections(arr){ localStorage.setItem(KEY_INSPECTIONS, JSON.stringify(arr)); } |
| | function loadManagers(){ return JSON.parse(localStorage.getItem(KEY_MANAGERS) || '[]'); } |
| | function saveManagers(arr){ localStorage.setItem(KEY_MANAGERS, JSON.stringify(arr)); } |
| | |
| | const inspectorsTableBody = document.querySelector('#inspectorsTable tbody'); |
| | const managersTableBody = document.querySelector('#managersTable tbody'); |
| | const btnAddInspector = document.getElementById('btnAddInspector'); |
| | const btnClearInspector = document.getElementById('btnClearInspector'); |
| | const mgrNational = document.getElementById('mgr_national'); |
| | const mgrName = document.getElementById('mgr_name'); |
| | const mgrFamily = document.getElementById('mgr_family'); |
| | const mgrPersonnel = document.getElementById('mgr_personnel'); |
| | const inspPersonnel = document.getElementById('insp_personnel'); |
| | const inspectorInfo = document.getElementById('inspectorInfo'); |
| | const workshopName = document.getElementById('workshopName'); |
| | const activityField = document.getElementById('activityField'); |
| | const employerName = document.getElementById('employerName'); |
| | const employerNationalId = document.getElementById('employerNationalId'); |
| | const numEmployees = document.getElementById('numEmployees'); |
| | const workersList = document.getElementById('workersList'); |
| | const lastInspectionDate = document.getElementById('lastInspectionDate'); |
| | const gpsField = document.getElementById('gpsField'); |
| | const btnGetGPS = document.getElementById('btnGetGPS'); |
| | const btnManualGPS = document.getElementById('btnManualGPS'); |
| | const btnSubmitInspection = document.getElementById('btnSubmitInspection'); |
| | const btnReset = document.getElementById('btnReset'); |
| | const inspectionsTableBody = document.querySelector('#inspectionsTable tbody'); |
| | const exportBtn = document.getElementById('exportBtn'); |
| | const exportMenu = document.getElementById('exportMenu'); |
| | const exportClose = document.getElementById('exportClose'); |
| | const exportOptions = document.querySelectorAll('.export-option'); |
| | const formMessage = document.getElementById('formMessage'); |
| | |
| | |
| | const inspectionComments = document.getElementById('inspectionComments'); |
| | const btnSendToManager = document.getElementById('btnSendToManager'); |
| | const btnSaveData = document.getElementById('btnSaveData'); |
| | const btnAddManager = document.getElementById('btnAddManager'); |
| | const btnClearManager = document.getElementById('btnClearManager'); |
| | const mgtNational = document.getElementById('mgt_national'); |
| | const mgtName = document.getElementById('mgt_name'); |
| | const mgtFamily = document.getElementById('mgt_family'); |
| | const mgtPosition = document.getElementById('mgt_position'); |
| | const mgtEmail = document.getElementById('mgt_email'); |
| | const mgtPhone = document.getElementById('mgt_phone'); |
| | const mgtTelegram = document.getElementById('mgt_telegram'); |
| | const shareModal = document.getElementById('shareModal'); |
| | const shareManager = document.getElementById('shareManager'); |
| | const shareMethod = document.getElementById('shareMethod'); |
| | const shareMessage = document.getElementById('shareMessage'); |
| | const btnSendShare = document.getElementById('btnSendShare'); |
| | const btnCancelShare = document.getElementById('btnCancelShare'); |
| | const modalClose = document.querySelector('.close'); |
| | |
| | |
| | const btnRefreshAnalysis = document.getElementById('btnRefreshAnalysis'); |
| | const totalInspectionsStat = document.getElementById('totalInspectionsStat'); |
| | const totalInspectorsStat = document.getElementById('totalInspectorsStat'); |
| | const avgEmployeesStat = document.getElementById('avgEmployeesStat'); |
| | const totalWorkersStat = document.getElementById('totalWorkersStat'); |
| | const activityFieldChart = document.getElementById('activityFieldChart'); |
| | const inspectionsOverTimeChart = document.getElementById('inspectionsOverTimeChart'); |
| | const advancedInspectorStats = document.getElementById('advancedInspectorStats'); |
| | const employeeDistributionChart = document.getElementById('employeeDistributionChart'); |
| | const btnShowMap = document.getElementById('btnShowMap'); |
| | |
| | |
| | const searchForm = document.getElementById('searchForm'); |
| | const searchInspector = document.getElementById('searchInspector'); |
| | const searchWorkshopName = document.getElementById('searchWorkshopName'); |
| | const searchActivityField = document.getElementById('searchActivityField'); |
| | const searchEmployerName = document.getElementById('searchEmployerName'); |
| | const searchMinEmployees = document.getElementById('searchMinEmployees'); |
| | const searchMaxEmployees = document.getElementById('searchMaxEmployees'); |
| | const btnRunSearch = document.getElementById('btnRunSearch'); |
| | const btnClearSearch = document.getElementById('btnClearSearch'); |
| | const btnExportFiltered = document.getElementById('btnExportFiltered'); |
| | const searchResultsTableBody = document.querySelector('#searchResultsTable tbody'); |
| | const searchResultSummary = document.getElementById('searchResultSummary'); |
| | const searchMessageEl = document.getElementById('searchMessage'); |
| | |
| | |
| | const btnImportData = document.getElementById('btnImportData'); |
| | const fileImporter = document.getElementById('fileImporter'); |
| | const btnExportAllData = document.getElementById('btnExportAllData'); |
| | const btnClearAllData = document.getElementById('btnClearAllData'); |
| | |
| | const { toJalaali } = window.jalaali; |
| | function toJalaliString(d = new Date()){ |
| | const date = (d instanceof Date) ? d : new Date(d); |
| | const j = toJalaali(date.getFullYear(), date.getMonth()+1, date.getDate()); |
| | return `${j.jy}/${String(j.jm).padStart(2,'0')}/${String(j.jd).padStart(2,'0')}`; |
| | } |
| | |
| | |
| | function convertToPlusCode(gpsString) { |
| | if (!gpsString || gpsString.trim() === '') { |
| | return ''; |
| | } |
| | |
| | try { |
| | const parts = gpsString.split(','); |
| | if (parts.length !== 2) { |
| | return gpsString; |
| | } |
| | |
| | const lat = parseFloat(parts[0].trim()); |
| | const lon = parseFloat(parts[1].trim()); |
| | |
| | if (isNaN(lat) || isNaN(lon)) { |
| | return gpsString; |
| | } |
| | |
| | |
| | const plusCode = OpenLocationCode.encode(lat, lon, 10); |
| | return plusCode; |
| | } catch (error) { |
| | console.error('خطا در تبدیل به Plus Code:', error); |
| | return gpsString; |
| | } |
| | } |
| | |
| | |
| | let editingInspectorIndex = -1; |
| | let editingInspectionIndex = -1; |
| | let editingManagerIndex = -1; |
| | let currentInspector = null; |
| | let filteredInspections = []; |
| | |
| | |
| | function renderInspectors(){ |
| | const list = loadInspectors(); |
| | inspectorsTableBody.innerHTML = ''; |
| | list.forEach((it, idx) => { |
| | const tr = document.createElement('tr'); |
| | tr.innerHTML = `<td>${idx+1}</td><td>${it.national}</td><td>${it.name}</td><td>${it.family}</td><td>${it.personnel}</td> |
| | <td> |
| | <button type="button" class="edit-inspector" data-idx="${idx}">ویرایش</button> |
| | <button type="button" class="delete-inspector btn-danger" data-idx="${idx}">حذف</button> |
| | </td>`; |
| | inspectorsTableBody.appendChild(tr); |
| | }); |
| | document.querySelectorAll('.edit-inspector').forEach(b => b.addEventListener('click', e => { |
| | const idx = Number(e.currentTarget.dataset.idx); |
| | const list = loadInspectors(); |
| | const rec = list[idx]; |
| | mgrNational.value = rec.national; mgrName.value = rec.name; mgrFamily.value = rec.family; mgrPersonnel.value = rec.personnel; |
| | editingInspectorIndex = idx; |
| | activateTab('inspectorsTab'); |
| | })); |
| | document.querySelectorAll('.delete-inspector').forEach(b => b.addEventListener('click', e => { |
| | const idx = Number(e.currentTarget.dataset.idx); |
| | if(!confirm('آیا مطمئن هستید حذف شود؟')) return; |
| | const arr = loadInspectors(); arr.splice(idx,1); saveInspectors(arr); renderInspectors(); |
| | if(editingInspectorIndex === idx){ mgrNational.value=''; mgrName.value=''; mgrFamily.value=''; mgrPersonnel.value=''; editingInspectorIndex=-1; } |
| | })); |
| | } |
| | |
| | |
| | function renderManagers() { |
| | const list = loadManagers(); |
| | managersTableBody.innerHTML = ''; |
| | list.forEach((it, idx) => { |
| | const tr = document.createElement('tr'); |
| | tr.innerHTML = `<td>${idx+1}</td><td>${it.national}</td><td>${it.name}</td><td>${it.family}</td><td>${it.position}</td><td>${it.email}</td><td>${it.phone}</td><td>${it.telegram || '-'}</td> |
| | <td> |
| | <button type="button" class="edit-manager" data-idx="${idx}">ویرایش</button> |
| | <button type="button" class="delete-manager btn-danger" data-idx="${idx}">حذف</button> |
| | </td>`; |
| | managersTableBody.appendChild(tr); |
| | }); |
| | |
| | |
| | updateShareManagerOptions(); |
| | |
| | document.querySelectorAll('.edit-manager').forEach(b => b.addEventListener('click', e => { |
| | const idx = Number(e.currentTarget.dataset.idx); |
| | const list = loadManagers(); |
| | const rec = list[idx]; |
| | mgtNational.value = rec.national; |
| | mgtName.value = rec.name; |
| | mgtFamily.value = rec.family; |
| | mgtPosition.value = rec.position; |
| | mgtEmail.value = rec.email; |
| | mgtPhone.value = rec.phone; |
| | mgtTelegram.value = rec.telegram || ''; |
| | editingManagerIndex = idx; |
| | activateTab('managersTab'); |
| | })); |
| | |
| | document.querySelectorAll('.delete-manager').forEach(b => b.addEventListener('click', e => { |
| | const idx = Number(e.currentTarget.dataset.idx); |
| | if(!confirm('آیا مطمئن هستید حذف شود؟')) return; |
| | const arr = loadManagers(); |
| | arr.splice(idx,1); |
| | saveManagers(arr); |
| | renderManagers(); |
| | if(editingManagerIndex === idx){ |
| | mgtNational.value=''; |
| | mgtName.value=''; |
| | mgtFamily.value=''; |
| | mgtPosition.value=''; |
| | mgtEmail.value=''; |
| | mgtPhone.value=''; |
| | mgtTelegram.value=''; |
| | editingManagerIndex=-1; |
| | } |
| | })); |
| | } |
| | |
| | |
| | function updateShareManagerOptions() { |
| | const managers = loadManagers(); |
| | shareManager.innerHTML = '<option value="">-- انتخاب کنید --</option>'; |
| | |
| | managers.forEach(manager => { |
| | const option = document.createElement('option'); |
| | option.value = manager.national; |
| | option.textContent = `${manager.name} ${manager.family} - ${manager.position}`; |
| | shareManager.appendChild(option); |
| | }); |
| | |
| | |
| | btnSendShare.disabled = !shareManager.value; |
| | } |
| | |
| | |
| | btnAddInspector.addEventListener('click', () => { |
| | const national = mgrNational.value.trim(), name = mgrName.value.trim(), family = mgrFamily.value.trim(), personnel = mgrPersonnel.value.trim(); |
| | if(!national || !name || !family || !personnel){ alert('لطفاً همه فیلدها را کامل کنید.'); return; } |
| | const arr = loadInspectors(); |
| | const dup = arr.some((it,i) => (it.national === national || it.personnel === personnel) && i !== editingInspectorIndex); |
| | if(dup){ alert('کد ملی یا شماره پرسنلی تکراری است.'); return; } |
| | const rec = { national, name, family, personnel }; |
| | if(editingInspectorIndex > -1){ arr[editingInspectorIndex] = rec; editingInspectorIndex = -1; } else arr.push(rec); |
| | saveInspectors(arr); renderInspectors(); mgrNational.value=''; mgrName.value=''; mgrFamily.value=''; mgrPersonnel.value=''; alert('ذخیره شد.'); |
| | }); |
| | btnClearInspector.addEventListener('click', ()=>{ mgrNational.value=''; mgrName.value=''; mgrFamily.value=''; mgrPersonnel.value=''; editingInspectorIndex=-1; }); |
| | |
| | |
| | btnAddManager.addEventListener('click', () => { |
| | const national = mgtNational.value.trim(), name = mgtName.value.trim(), family = mgtFamily.value.trim(), |
| | position = mgtPosition.value.trim(), email = mgtEmail.value.trim(), phone = mgtPhone.value.trim(), |
| | telegram = mgtTelegram.value.trim(); |
| | if(!national || !name || !family || !position || !email || !phone){ alert('لطفاً فیلدهای ضروری را کامل کنید.'); return; } |
| | const arr = loadManagers(); |
| | const dup = arr.some((it,i) => (it.national === national || it.email === email) && i !== editingManagerIndex); |
| | if(dup){ alert('کد ملی یا ایمیل تکراری است.'); return; } |
| | const rec = { national, name, family, position, email, phone, telegram }; |
| | if(editingManagerIndex > -1){ arr[editingManagerIndex] = rec; editingManagerIndex = -1; } else arr.push(rec); |
| | saveManagers(arr); renderManagers(); |
| | mgtNational.value=''; mgtName.value=''; mgtFamily.value=''; mgtPosition.value=''; |
| | mgtEmail.value=''; mgtPhone.value=''; mgtTelegram.value=''; |
| | alert('ذخیره شد.'); |
| | }); |
| | btnClearManager.addEventListener('click', ()=>{ |
| | mgtNational.value=''; mgtName.value=''; mgtFamily.value=''; mgtPosition.value=''; |
| | mgtEmail.value=''; mgtPhone.value=''; mgtTelegram.value=''; |
| | editingManagerIndex=-1; |
| | }); |
| | |
| | |
| | function showMessage(msg, duration = 3000, element = formMessage) { |
| | element.textContent = msg; |
| | element.style.display = 'block'; |
| | setTimeout(() => { |
| | element.style.display = 'none'; |
| | }, duration); |
| | } |
| | |
| | |
| | inspPersonnel.addEventListener('input', () => { |
| | const personnel = inspPersonnel.value.trim(); |
| | if (!personnel) { |
| | inspectorInfo.textContent = ''; |
| | currentInspector = null; |
| | btnSubmitInspection.disabled = true; |
| | return; |
| | } |
| | |
| | const inspectors = loadInspectors(); |
| | const found = inspectors.find(it => it.personnel === personnel); |
| | |
| | if (found) { |
| | currentInspector = found; |
| | inspectorInfo.innerHTML = `بازرس: <strong>${found.name} ${found.family}</strong>`; |
| | btnSubmitInspection.disabled = false; |
| | } else { |
| | inspectorInfo.textContent = 'بازرسی با این شماره پرسنلی یافت نشد.'; |
| | currentInspector = null; |
| | btnSubmitInspection.disabled = true; |
| | } |
| | }); |
| | |
| | |
| | numEmployees.addEventListener('input', () => { |
| | const count = parseInt(numEmployees.value) || 0; |
| | workersList.innerHTML = ''; |
| | |
| | if (count > 0) { |
| | workersList.innerHTML = '<h4 style="margin:0 0 12px 0">لیست کارگران</h4>'; |
| | |
| | for (let i = 1; i <= count; i++) { |
| | const workerBlock = document.createElement('div'); |
| | workerBlock.className = 'worker-block'; |
| | workerBlock.innerHTML = ` |
| | <h5 style="margin:0 0 12px 0">کارگر شماره ${i}</h5> |
| | <div class="grid"> |
| | <div> |
| | <label>نام</label> |
| | <input type="text" id="worker_name_${i}" /> |
| | </div> |
| | <div> |
| | <label>نام خانوادگی</label> |
| | <input type="text" id="worker_family_${i}" /> |
| | </div> |
| | <div class="optional-field"> |
| | <label>کد ملی</label> |
| | <input type="text" id="worker_national_${i}" /> |
| | </div> |
| | <div class="optional-field"> |
| | <label>شماره بیمه</label> |
| | <input type="text" id="worker_insurance_${i}" /> |
| | </div> |
| | </div> |
| | `; |
| | workersList.appendChild(workerBlock); |
| | } |
| | } |
| | }); |
| | |
| | |
| | btnGetGPS.addEventListener('click', () => { |
| | if (navigator.geolocation) { |
| | btnGetGPS.disabled = true; |
| | btnGetGPS.textContent = 'در حال دریافت مختصات...'; |
| | |
| | navigator.geolocation.getCurrentPosition( |
| | position => { |
| | const lat = position.coords.latitude; |
| | const lon = position.coords.longitude; |
| | gpsField.value = `${lat.toFixed(6)}, ${lon.toFixed(6)}`; |
| | btnGetGPS.disabled = false; |
| | btnGetGPS.textContent = 'ثبت مختصات GPS'; |
| | showMessage('مختصات GPS با موفقیت ثبت شد.'); |
| | }, |
| | error => { |
| | |
| | fetch('https://ipapi.co/json/') |
| | .then(response => response.json()) |
| | .then(data => { |
| | if (data.latitude && data.longitude) { |
| | gpsField.value = `${data.latitude}, ${data.longitude}`; |
| | showMessage('مختصات تقریبی از طریق IP دریافت شد.'); |
| | } else { |
| | showMessage('خطا در دریافت مختصات. لطفاً مختصات را به صورت دستی وارد کنید.'); |
| | } |
| | }) |
| | .catch(err => { |
| | showMessage('خطا در دریافت مختصات. لطفاً مختصات را به صورت دستی وارد کنید.'); |
| | }) |
| | .finally(() => { |
| | btnGetGPS.disabled = false; |
| | btnGetGPS.textContent = 'ثبت مختصات GPS'; |
| | }); |
| | } |
| | ); |
| | } else { |
| | showMessage('مرورگر شما از GPS پشتیبانی نمیکند. لطفاً مختصات را به صورت دستی وارد کنید.'); |
| | } |
| | }); |
| | |
| | |
| | btnManualGPS.addEventListener('click', () => { |
| | const manualGps = prompt('لطفاً مختصات GPS را به فرمت lat,lon وارد کنید:'); |
| | if (manualGps) { |
| | |
| | const parts = manualGps.split(','); |
| | if (parts.length === 2 && !isNaN(parseFloat(parts[0])) && !isNaN(parseFloat(parts[1]))) { |
| | gpsField.value = manualGps; |
| | showMessage('مختصات با موفقیت ثبت شد.'); |
| | } else { |
| | showMessage('فرمت مختصات نامعتبر است. لطفاً از فرمت lat,lon استفاده کنید.'); |
| | } |
| | } |
| | }); |
| | |
| | |
| | btnSubmitInspection.addEventListener('click', () => { |
| | if (!currentInspector) { |
| | showMessage('لطفاً شماره پرسنلی بازرس را وارد کنید.'); |
| | return; |
| | } |
| | |
| | if (!workshopName.value.trim()) { |
| | showMessage('لطفاً نام کارگاه را وارد کنید.'); |
| | return; |
| | } |
| | |
| | if (!gpsField.value.trim()) { |
| | showMessage('لطفاً مختصات GPS را ثبت کنید.'); |
| | return; |
| | } |
| | |
| | const inspection = { |
| | id: Date.now(), |
| | personnel: inspPersonnel.value.trim(), |
| | inspectorName: `${currentInspector.name} ${currentInspector.family}`, |
| | workshopName: workshopName.value.trim(), |
| | activityField: activityField.value.trim(), |
| | employerName: employerName.value.trim(), |
| | employerNationalId: employerNationalId.value.trim(), |
| | numEmployees: parseInt(numEmployees.value) || 0, |
| | workers: [], |
| | gps: gpsField.value.trim(), |
| | plusCode: convertToPlusCode(gpsField.value.trim()), |
| | date: new Date().toISOString(), |
| | jalaliDate: toJalaliString(), |
| | comments: inspectionComments.value.trim() |
| | }; |
| | |
| | |
| | if (inspection.numEmployees > 0) { |
| | for (let i = 1; i <= inspection.numEmployees; i++) { |
| | const name = document.getElementById(`worker_name_${i}`)?.value.trim() || ''; |
| | const family = document.getElementById(`worker_family_${i}`)?.value.trim() || ''; |
| | const national = document.getElementById(`worker_national_${i}`)?.value.trim() || ''; |
| | const insurance = document.getElementById(`worker_insurance_${i}`)?.value.trim() || ''; |
| | |
| | inspection.workers.push({ name, family, national, insurance }); |
| | } |
| | } |
| | |
| | const inspections = loadInspections(); |
| | if(editingInspectionIndex > -1){ |
| | inspections[editingInspectionIndex] = inspection; |
| | editingInspectionIndex = -1; |
| | showMessage('بازرسی با موفقیت بروزرسانی شد.'); |
| | } else { |
| | inspections.push(inspection); |
| | showMessage('بازرسی با موفقیت ثبت شد.'); |
| | } |
| | saveInspections(inspections); |
| | |
| | |
| | lastInspectionDate.textContent = inspection.jalaliDate; |
| | |
| | renderInspections(); |
| | |
| | |
| | document.getElementById('inspectionForm').reset(); |
| | workersList.innerHTML = ''; |
| | inspectorInfo.textContent = ''; |
| | lastInspectionDate.textContent = 'هنوز بازرسی ثبت نشده'; |
| | currentInspector = null; |
| | btnSubmitInspection.disabled = true; |
| | |
| | |
| | if(loadInspections().length > 0){ |
| | btnSendToManager.disabled = false; |
| | exportBtn.disabled = false; |
| | } |
| | renderAnalysis(); |
| | }); |
| | |
| | |
| | btnReset.addEventListener('click', () => { |
| | if (confirm('آیا از پاک کردن فرم مطمئن هستید؟')) { |
| | |
| | document.getElementById('inspectionForm').reset(); |
| | workersList.innerHTML = ''; |
| | inspectorInfo.textContent = ''; |
| | lastInspectionDate.textContent = 'هنوز بازرسی ثبت نشده'; |
| | currentInspector = null; |
| | |
| | showMessage('فرم با موفقیت پاک شد.'); |
| | } |
| | }); |
| | |
| | |
| | function renderInspections(inspections = loadInspections(), tableBody = inspectionsTableBody) { |
| | tableBody.innerHTML = ''; |
| | |
| | inspections.forEach((inspection, index) => { |
| | const tr = document.createElement('tr'); |
| | const displayGps = inspection.plusCode || inspection.gps; |
| | |
| | tr.innerHTML = ` |
| | <td>${index + 1}</td> |
| | <td>${inspection.personnel}</td> |
| | <td>${inspection.inspectorName}</td> |
| | <td>${inspection.workshopName}</td> |
| | <td>${inspection.numEmployees}</td> |
| | <td>${displayGps}</td> |
| | <td>${inspection.jalaliDate}</td> |
| | <td> |
| | <button type="button" class="edit-inspection" data-id="${inspection.id}">ویرایش</button> |
| | <button type="button" class="delete-inspection btn-danger" data-id="${inspection.id}">حذف</button> |
| | </td> |
| | `; |
| | |
| | tableBody.appendChild(tr); |
| | }); |
| | |
| | document.querySelectorAll('.edit-inspection').forEach(button => { |
| | button.addEventListener('click', handleEditInspection); |
| | }); |
| | |
| | document.querySelectorAll('.delete-inspection').forEach(button => { |
| | button.addEventListener('click', handleDeleteInspection); |
| | }); |
| | |
| | const allInspections = loadInspections(); |
| | if(allInspections.length > 0){ |
| | btnSendToManager.disabled = false; |
| | exportBtn.disabled = false; |
| | } else { |
| | btnSendToManager.disabled = true; |
| | exportBtn.disabled = true; |
| | } |
| | } |
| | |
| | |
| | function handleEditInspection(e) { |
| | const inspectionId = Number(e.target.dataset.id); |
| | const inspections = loadInspections(); |
| | const inspectionIndex = inspections.findIndex(insp => insp.id === inspectionId); |
| | |
| | if (inspectionIndex > -1) { |
| | const inspection = inspections[inspectionIndex]; |
| | inspPersonnel.value = inspection.personnel; |
| | inspPersonnel.dispatchEvent(new Event('input')); |
| | workshopName.value = inspection.workshopName; |
| | activityField.value = inspection.activityField; |
| | employerName.value = inspection.employerName; |
| | employerNationalId.value = inspection.employerNationalId; |
| | numEmployees.value = inspection.numEmployees; |
| | gpsField.value = inspection.gps; |
| | inspectionComments.value = inspection.comments; |
| | lastInspectionDate.textContent = inspection.jalaliDate; |
| | btnSubmitInspection.disabled = false; |
| | numEmployees.dispatchEvent(new Event('input')); |
| | inspection.workers.forEach((worker, i) => { |
| | const workerIndex = i + 1; |
| | if (document.getElementById(`worker_name_${workerIndex}`)) { |
| | document.getElementById(`worker_name_${workerIndex}`).value = worker.name; |
| | document.getElementById(`worker_family_${workerIndex}`).value = worker.family; |
| | document.getElementById(`worker_national_${workerIndex}`).value = worker.national; |
| | document.getElementById(`worker_insurance_${workerIndex}`).value = worker.insurance; |
| | } |
| | }); |
| | editingInspectionIndex = inspectionIndex; |
| | showMessage(`در حال ویرایش بازرسی کارگاه: ${inspection.workshopName}`); |
| | activateTab('inspectionTab'); |
| | window.scrollTo(0, 0); |
| | } |
| | } |
| | |
| | |
| | function handleDeleteInspection(e) { |
| | const inspectionId = Number(e.target.dataset.id); |
| | if (confirm(`آیا از حذف این بازرسی مطمئن هستید؟`)) { |
| | let inspections = loadInspections(); |
| | inspections = inspections.filter(insp => insp.id !== inspectionId); |
| | saveInspections(inspections); |
| | renderInspections(); |
| | showMessage('بازرسی با موفقیت حذف شد.'); |
| | renderAnalysis(); |
| | |
| | if(document.getElementById('searchTab').classList.contains('active')) { |
| | handleSearch(); |
| | } |
| | } |
| | } |
| | |
| | |
| | btnSaveData.addEventListener('click', () => { |
| | const data = { |
| | inspectors: loadInspectors(), |
| | inspections: loadInspections(), |
| | managers: loadManagers(), |
| | exportDate: new Date().toISOString() |
| | }; |
| | |
| | const dataStr = JSON.stringify(data, null, 2); |
| | const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); |
| | |
| | const exportFileDefaultName = `backup_Tamin_${toJalaliString().replace(/\//g, '-')}.json`; |
| | |
| | const linkElement = document.createElement('a'); |
| | linkElement.setAttribute('href', dataUri); |
| | linkElement.setAttribute('download', exportFileDefaultName); |
| | linkElement.click(); |
| | |
| | showMessage('فایل پشتیبان با موفقیت ایجاد شد.'); |
| | }); |
| | |
| | |
| | exportBtn.addEventListener('click', () => { |
| | exportMenu.style.display = 'flex'; |
| | document.body.classList.add('modal-open'); |
| | }); |
| | |
| | exportClose.addEventListener('click', () => { |
| | exportMenu.style.display = 'none'; |
| | document.body.classList.remove('modal-open'); |
| | }); |
| | |
| | exportOptions.forEach(option => { |
| | option.addEventListener('click', () => { |
| | const format = option.dataset.format; |
| | exportMenu.style.display = 'none'; |
| | document.body.classList.remove('modal-open'); |
| | |
| | const inspectionsToExport = loadInspections(); |
| | |
| | switch (format) { |
| | case 'excel': |
| | exportToExcel(inspectionsToExport); |
| | break; |
| | case 'word': |
| | exportToWord(inspectionsToExport); |
| | break; |
| | case 'pdf': |
| | exportToPDF(inspectionsToExport); |
| | break; |
| | case 'text': |
| | exportToText(inspectionsToExport); |
| | break; |
| | } |
| | }); |
| | }); |
| | |
| | |
| | function exportToExcel(inspections) { |
| | if (inspections.length === 0) { |
| | alert('هیچ بازرسی برای خروجی گرفتن وجود ندارد.'); |
| | return; |
| | } |
| | const data = inspections.map((inspection, index) => { |
| | let workersInfo = (inspection.workers && inspection.workers.length > 0) |
| | ? inspection.workers.map((w, i) => `${i + 1}. ${w.name} ${w.family} (کدملی: ${w.national || '-'})`).join('\n') |
| | : 'ندارد'; |
| | return [ |
| | index + 1, inspection.personnel, inspection.inspectorName, inspection.workshopName, |
| | inspection.activityField, inspection.employerName, inspection.employerNationalId, |
| | inspection.numEmployees, inspection.plusCode || '', inspection.gps, inspection.jalaliDate, |
| | inspection.comments, workersInfo |
| | ]; |
| | }); |
| | |
| | data.unshift([ |
| | '#', 'شماره پرسنلی', 'بازرس', 'نام کارگاه', 'رسته فعالیت', |
| | 'نام کارفرما', 'کد ملی کارفرما', 'تعداد نیرو', 'مختصات (Plus Code)', |
| | 'مختصات (GPS)', 'تاریخ شمسی', 'توضیحات', 'اطلاعات کارگران' |
| | ]); |
| | |
| | const ws = XLSX.utils.aoa_to_sheet(data); |
| | |
| | |
| | for (let i = 0; i < inspections.length; i++) { |
| | const inspection = inspections[i]; |
| | const rowIndex = i + 1; |
| | |
| | if (inspection.plusCode) { |
| | const plusCodeCellAddress = XLSX.utils.encode_cell({ r: rowIndex, c: 8 }); |
| | if (!ws[plusCodeCellAddress]) ws[plusCodeCellAddress] = {}; |
| | ws[plusCodeCellAddress].l = { Target: `https://www.google.com/maps/search/${encodeURIComponent(inspection.plusCode)}`, Tooltip: 'نمایش Plus Code در نقشه' }; |
| | } |
| | if (inspection.gps) { |
| | const gpsCellAddress = XLSX.utils.encode_cell({ r: rowIndex, c: 9 }); |
| | if (!ws[gpsCellAddress]) ws[gpsCellAddress] = {}; |
| | ws[gpsCellAddress].l = { Target: `https://maps.google.com/?q=${encodeURIComponent(inspection.gps)}`, Tooltip: 'نمایش GPS در نقشه' }; |
| | } |
| | } |
| | |
| | ws['!cols'] = [ |
| | { wch: 5 }, { wch: 15 }, { wch: 20 }, { wch: 25 }, { wch: 20 }, { wch: 20 }, |
| | { wch: 15 }, { wch: 10 }, { wch: 20 }, { wch: 25 }, { wch: 12 }, { wch: 30 }, { wch: 50 } |
| | ]; |
| | |
| | const wb = XLSX.utils.book_new(); |
| | XLSX.utils.book_append_sheet(wb, ws, 'بازرسیها'); |
| | const fileName = `inspections_${toJalaliString().replace(/\//g, '-')}.xlsx`; |
| | XLSX.writeFile(wb, fileName); |
| | showMessage('خروجی Excel با موفقیت ایجاد شد.'); |
| | } |
| | |
| | |
| | function exportToWord(inspections) { |
| | if (inspections.length === 0) { alert('هیچ بازرسی برای خروجی گرفتن وجود ندارد.'); return; } |
| | let html = ` |
| | <!DOCTYPE html><html dir="rtl"><head><meta charset="UTF-8"><title>گزارش بازرسیها</title> |
| | <style>body{font-family:'Tahoma',sans-serif;direction:rtl;}table{border-collapse:collapse;width:100%;}th,td{border:1px solid #ddd;padding:8px;text-align:right;}th{background-color:#f2f2f2;}h1{color:#4361ee;}</style> |
| | </head><body><h1>گزارش بازرسیها</h1><p>تاریخ: ${toJalaliString()}</p> |
| | <table><thead><tr><th>#</th><th>بازرس</th><th>کارگاه</th><th>کارفرما</th><th>تعداد نیرو</th><th>Plus Code</th><th>GPS</th><th>تاریخ</th></tr></thead><tbody>`; |
| | |
| | inspections.forEach((inspection, index) => { |
| | const plusCodeLink = inspection.plusCode ? `<a href="https://www.google.com/maps/search/${encodeURIComponent(inspection.plusCode)}" target="_blank">${inspection.plusCode}</a>` : ''; |
| | const gpsLink = inspection.gps ? `<a href="https://maps.google.com/?q=${encodeURIComponent(inspection.gps)}" target="_blank">${inspection.gps}</a>` : ''; |
| | html += `<tr> |
| | <td>${index + 1}</td> |
| | <td>${inspection.inspectorName}</td> |
| | <td>${inspection.workshopName}</td> |
| | <td>${inspection.employerName}</td> |
| | <td>${inspection.numEmployees}</td> |
| | <td>${plusCodeLink}</td> |
| | <td>${gpsLink}</td> |
| | <td>${inspection.jalaliDate}</td> |
| | </tr>`; |
| | }); |
| | |
| | html += `</tbody></table></body></html>`; |
| | const blob = new Blob([html], { type: 'application/msword' }); |
| | const url = URL.createObjectURL(blob); |
| | const a = document.createElement('a'); |
| | a.href = url; |
| | a.download = `inspections_${toJalaliString().replace(/\//g, '-')}.doc`; |
| | a.click(); |
| | URL.revokeObjectURL(url); |
| | showMessage('خروجی Word با موفقیت ایجاد شد.'); |
| | } |
| | |
| | |
| | function exportToPDF(inspections) { |
| | if (inspections.length === 0) { alert('هیچ بازرسی برای خروجی گرفتن وجود ندارد.'); return; } |
| | const reportWindow = window.open('', '_blank'); |
| | let html = ` |
| | <!DOCTYPE html><html dir="rtl"><head><meta charset="UTF-8"><title>گزارش بازرسیها</title> |
| | <style>body{font-family:'Tahoma',sans-serif;direction:rtl;margin:20px;}table{border-collapse:collapse;width:100%;margin-top:20px;}th,td{border:1px solid #ddd;padding:8px;text-align:right;}th{background-color:#f2f2f2;}h1{color:#4361ee;text-align:center;}@media print{.no-print{display:none;}} a{color:#4361ee;text-decoration:none;} a:hover{text-decoration:underline;}</style> |
| | </head><body><div style="text-align:center;"><h1>گزارش بازرسیها</h1><p>تاریخ: ${toJalaliString()}</p></div> |
| | <table><thead><tr><th>#</th><th>بازرس</th><th>کارگاه</th><th>کارفرما</th><th>تعداد نیرو</th><th>Plus Code</th><th>GPS</th><th>تاریخ</th></tr></thead><tbody>`; |
| | |
| | inspections.forEach((inspection, index) => { |
| | const plusCodeLink = inspection.plusCode ? `<a href="https://www.google.com/maps/search/${encodeURIComponent(inspection.plusCode)}" target="_blank">${inspection.plusCode}</a>` : ''; |
| | const gpsLink = inspection.gps ? `<a href="https://maps.google.com/?q=${encodeURIComponent(inspection.gps)}" target="_blank">${inspection.gps}</a>` : ''; |
| | html += `<tr> |
| | <td>${index + 1}</td> |
| | <td>${inspection.inspectorName}</td> |
| | <td>${inspection.workshopName}</td> |
| | <td>${inspection.employerName}</td> |
| | <td>${inspection.numEmployees}</td> |
| | <td>${plusCodeLink}</td> |
| | <td>${gpsLink}</td> |
| | <td>${inspection.jalaliDate}</td> |
| | </tr>`; |
| | }); |
| | |
| | html += `</tbody></table><div class="no-print" style="text-align:center;margin-top:30px;"><button onclick="window.print()">چاپ</button></div></body></html>`; |
| | reportWindow.document.write(html); |
| | reportWindow.document.close(); |
| | showMessage('گزارش چاپی در پنجره جدید باز شد.'); |
| | } |
| | |
| | |
| | function exportToText(inspections) { |
| | if (inspections.length === 0) { alert('هیچ بازرسی برای خروجی گرفتن وجود ندارد.'); return; } |
| | let text = `گزارش بازرسیها - تاریخ: ${toJalaliString()}\n========================================\n\n`; |
| | |
| | inspections.forEach((inspection, index) => { |
| | text += `بازرسی شماره ${index + 1}:\n`; |
| | text += `----------------------------------------\n`; |
| | text += `بازرس: ${inspection.inspectorName}\n`; |
| | text += `کارگاه: ${inspection.workshopName}\n`; |
| | text += `تعداد نیرو: ${inspection.numEmployees}\n`; |
| | if (inspection.plusCode) text += `Plus Code: ${inspection.plusCode} (لینک: https://www.google.com/maps/search/${encodeURIComponent(inspection.plusCode)})\n`; |
| | if (inspection.gps) text += `GPS: ${inspection.gps} (لینک: https://maps.google.com/?q=${encodeURIComponent(inspection.gps)})\n`; |
| | text += `تاریخ: ${inspection.jalaliDate}\n\n`; |
| | }); |
| | |
| | const blob = new Blob([text], { type: 'text/plain' }); |
| | const url = URL.createObjectURL(blob); |
| | const a = document.createElement('a'); |
| | a.href = url; |
| | a.download = `inspections_${toJalaliString().replace(/\//g, '-')}.txt`; |
| | a.click(); |
| | URL.revokeObjectURL(url); |
| | showMessage('خروجی متنی با موفقیت ایجاد شد.'); |
| | } |
| | |
| | |
| | |
| | btnSendToManager.addEventListener('click', () => { |
| | const inspections = loadInspections(); |
| | if (inspections.length === 0) { |
| | showMessage('هیچ بازرسی ثبت شدهای وجود ندارد.'); |
| | return; |
| | } |
| | shareModal.style.display = 'flex'; |
| | document.body.classList.add('modal-open'); |
| | }); |
| | |
| | |
| | |
| | modalClose.addEventListener('click', () => { |
| | shareModal.style.display = 'none'; |
| | document.body.classList.remove('modal-open'); |
| | }); |
| | |
| | btnCancelShare.addEventListener('click', () => { |
| | shareModal.style.display = 'none'; |
| | document.body.classList.remove('modal-open'); |
| | }); |
| | |
| | |
| | shareManager.addEventListener('change', () => { |
| | btnSendShare.disabled = !shareManager.value; |
| | }); |
| | |
| | |
| | function showModernMessage(message) { |
| | |
| | const existingAlert = document.getElementById('modernAlert'); |
| | if (existingAlert) { |
| | existingAlert.remove(); |
| | } |
| | |
| | const alertDiv = document.createElement('div'); |
| | alertDiv.id = 'modernAlert'; |
| | alertDiv.className = 'modern-alert'; |
| | alertDiv.innerHTML = ` |
| | <div class="spinner"></div> |
| | <p style="margin:0;">${message}</p> |
| | `; |
| | document.body.appendChild(alertDiv); |
| | |
| | |
| | setTimeout(() => { |
| | alertDiv.classList.add('show'); |
| | }, 10); |
| | |
| | return alertDiv; |
| | } |
| | |
| | |
| | function hideModernMessage(alertElement) { |
| | if (alertElement) { |
| | alertElement.classList.remove('show'); |
| | setTimeout(() => { |
| | alertElement.remove(); |
| | }, 500); |
| | } |
| | } |
| | |
| | |
| | btnSendShare.addEventListener('click', () => { |
| | const managerId = shareManager.value; |
| | const method = shareMethod.value; |
| | const message = shareMessage.value.trim(); |
| | |
| | if (!managerId) { |
| | showMessage('لطفاً یک مدیر انتخاب کنید.'); |
| | return; |
| | } |
| | |
| | const managers = loadManagers(); |
| | const manager = managers.find(m => m.national === managerId); |
| | |
| | if (!manager) { |
| | showMessage('مدیر انتخاب شده یافت نشد.'); |
| | return; |
| | } |
| | |
| | const reportContent = createFullReportContent(loadInspections()); |
| | let finalMessage = `گزارش تمام بازرسیها\n\n`; |
| | finalMessage += `تاریخ: ${toJalaliString()}\n`; |
| | if (message) { |
| | finalMessage += `\nپیام بازرس:\n${message}\n`; |
| | } |
| | finalMessage += `\n${reportContent}`; |
| | |
| | navigator.clipboard.writeText(finalMessage).then(() => { |
| | shareModal.style.display = 'none'; |
| | document.body.classList.remove('modal-open'); |
| | const modernAlert = showModernMessage('گزارش در کلیپبورد کپی شد.<br>لطفاً آن را در برنامه مورد نظر الصاق (Paste) کنید.'); |
| | |
| | setTimeout(() => { |
| | hideModernMessage(modernAlert); |
| | const subject = encodeURIComponent(`گزارش بازرسی - ${toJalaliString()}`); |
| | const openLink = (url) => { |
| | const a = document.createElement('a'); |
| | a.href = url; |
| | a.target = '_blank'; |
| | document.body.appendChild(a); |
| | a.click(); |
| | document.body.removeChild(a); |
| | }; |
| | |
| | switch (method) { |
| | case 'email': openLink(`mailto:${manager.email}?subject=${subject}`); break; |
| | case 'telegram': openLink(`tg://resolve?domain=${manager.telegram}`); break; |
| | case 'whatsapp': |
| | if (!manager.phone) { alert('شماره تلفنی برای این مدیر ثبت نشده است.'); return; } |
| | let phoneNumber = manager.phone.replace(/\D/g, ''); |
| | if (phoneNumber.startsWith('09')) { phoneNumber = '98' + phoneNumber.substring(1); } |
| | openLink(`https://wa.me/${phoneNumber}`); |
| | break; |
| | } |
| | shareMessage.value = ''; |
| | }, 5000); |
| | |
| | }).catch(err => { |
| | console.error('خطا در کپی کردن: ', err); |
| | showMessage('متاسفانه کپی کردن در کلیپبورد با خطا مواجه شد.'); |
| | }); |
| | }); |
| | |
| | |
| | function createFullReportContent(inspections) { |
| | let content = ''; |
| | inspections.forEach((inspection, index) => { |
| | content += `بازرسی شماره ${index + 1}:\n`; |
| | content += `----------------------------------------\n`; |
| | content += `بازرس: ${inspection.inspectorName} (${inspection.personnel})\n`; |
| | content += `کارگاه: ${inspection.workshopName} (رسته: ${inspection.activityField})\n`; |
| | content += `کارفرما: ${inspection.employerName} (کدملی: ${inspection.employerNationalId})\n`; |
| | content += `تعداد نیرو: ${inspection.numEmployees}\n`; |
| | content += `مختصات: ${inspection.plusCode || inspection.gps}\n`; |
| | content += `لینک نقشه: https://maps.google.com/?q=${encodeURIComponent(inspection.gps)}\n` |
| | content += `تاریخ: ${inspection.jalaliDate}\n`; |
| | content += `توضیحات: ${inspection.comments || 'ندارد'}\n`; |
| | if (inspection.workers && inspection.workers.length > 0) { |
| | content += `\nلیست کارگران:\n`; |
| | inspection.workers.forEach((w, i) => { |
| | content += `${i + 1}. ${w.name} ${w.family} (کدملی: ${w.national || '-'}, بیمه: ${w.insurance || '-'}) \n`; |
| | }); |
| | } |
| | content += `\n\n`; |
| | }); |
| | return content; |
| | } |
| | |
| | |
| | function renderAnalysis() { |
| | const inspections = loadInspections(); |
| | const inspectors = loadInspectors(); |
| | |
| | const placeholder = (el, type='chart') => { |
| | const message = '<p class="small-muted" style="text-align:center; padding: 20px;">هنوز بازرسی ثبت نشده است تا تحلیلی ارائه شود.</p>'; |
| | el.innerHTML = message; |
| | }; |
| | |
| | if (inspections.length === 0) { |
| | totalInspectionsStat.textContent = '0'; |
| | totalInspectorsStat.textContent = inspectors.length; |
| | avgEmployeesStat.textContent = '0'; |
| | totalWorkersStat.textContent = '0'; |
| | placeholder(activityFieldChart); |
| | placeholder(inspectionsOverTimeChart); |
| | placeholder(advancedInspectorStats, 'table'); |
| | placeholder(employeeDistributionChart); |
| | btnShowMap.disabled = true; |
| | return; |
| | } |
| | |
| | totalInspectionsStat.textContent = inspections.length; |
| | totalInspectorsStat.textContent = inspectors.length; |
| | const totalEmployees = inspections.reduce((sum, insp) => sum + insp.numEmployees, 0); |
| | totalWorkersStat.textContent = totalEmployees; |
| | avgEmployeesStat.textContent = (totalEmployees / inspections.length).toFixed(1); |
| | btnShowMap.disabled = false; |
| | |
| | const inspectionsByActivity = inspections.reduce((acc, insp) => { |
| | const activity = insp.activityField.trim() || 'نامشخص'; |
| | acc[activity] = (acc[activity] || 0) + 1; |
| | return acc; |
| | }, {}); |
| | const sortedActivities = Object.entries(inspectionsByActivity).sort((a, b) => b[1] - a[1]); |
| | let activityChartHtml = '<div class="bar-chart">'; |
| | if (sortedActivities.length > 0) { |
| | const maxActivityCount = sortedActivities[0][1]; |
| | sortedActivities.forEach(([activity, count]) => { |
| | const barWidth = Math.max((count / maxActivityCount) * 100, 5); |
| | activityChartHtml += `<div class="bar-item"><span class="bar-label" title="${activity}">${activity}</span><div class="bar-wrapper"><div class="bar" style="width: ${barWidth}%;">${count} مورد</div></div></div>`; |
| | }); |
| | } |
| | activityChartHtml += '</div>'; |
| | activityFieldChart.innerHTML = activityChartHtml; |
| | |
| | const inspectionsByDate = inspections.reduce((acc, insp) => { |
| | acc[insp.jalaliDate] = (acc[insp.jalaliDate] || 0) + 1; |
| | return acc; |
| | }, {}); |
| | const sortedDates = Object.entries(inspectionsByDate).sort((a, b) => a[0].localeCompare(b[0])); |
| | let timeChartHtml = '<div class="bar-chart">'; |
| | if (sortedDates.length > 0) { |
| | const maxCount = Math.max(...sortedDates.map(d => d[1])); |
| | sortedDates.forEach(([date, count]) => { |
| | const barWidth = Math.max((count / maxCount) * 100, 5); |
| | timeChartHtml += `<div class="bar-item"><span class="bar-label" title="${date}">${date}</span><div class="bar-wrapper"><div class="bar" style="width: ${barWidth}%; background: var(--gradient-success);">${count} بازرسی</div></div></div>`; |
| | }); |
| | } |
| | timeChartHtml += '</div>'; |
| | inspectionsOverTimeChart.innerHTML = timeChartHtml; |
| | |
| | const inspectorData = inspections.reduce((acc, insp) => { |
| | if (!acc[insp.inspectorName]) { |
| | acc[insp.inspectorName] = { count: 0, totalEmployees: 0 }; |
| | } |
| | acc[insp.inspectorName].count++; |
| | acc[insp.inspectorName].totalEmployees += insp.numEmployees; |
| | return acc; |
| | }, {}); |
| | const sortedInspectorsData = Object.entries(inspectorData).sort((a, b) => b[1].count - a[1].count); |
| | let inspectorTableHtml = ` |
| | <table aria-label="آمار پیشرفته بازرسان"> |
| | <thead><tr><th>#</th><th>نام بازرس</th><th>تعداد بازرسی</th><th>مجموع نیرو</th><th>میانگین نیرو/بازرسی</th></tr></thead> |
| | <tbody> |
| | `; |
| | sortedInspectorsData.forEach(([name, data], idx) => { |
| | const avg = (data.totalEmployees / data.count).toFixed(1); |
| | inspectorTableHtml += `<tr><td>${idx+1}</td><td>${name}</td><td>${data.count}</td><td>${data.totalEmployees}</td><td>${avg}</td></tr>`; |
| | }); |
| | inspectorTableHtml += '</tbody></table>'; |
| | advancedInspectorStats.innerHTML = inspectorTableHtml; |
| | |
| | const employeeBins = { "۱-۵ نفر": 0, "۶-۱۰ نفر": 0, "۱۱-۲۰ نفر": 0, "۲۱-۵۰ نفر": 0, "۵۰+ نفر": 0 }; |
| | inspections.forEach(insp => { |
| | const n = insp.numEmployees; |
| | if (n >= 1 && n <= 5) employeeBins["۱-۵ نفر"]++; |
| | else if (n >= 6 && n <= 10) employeeBins["۶-۱۰ نفر"]++; |
| | else if (n >= 11 && n <= 20) employeeBins["۱۱-۲۰ نفر"]++; |
| | else if (n >= 21 && n <= 50) employeeBins["۲۱-۵۰ نفر"]++; |
| | else if (n > 50) employeeBins["۵۰+ نفر"]++; |
| | }); |
| | const sortedBins = Object.entries(employeeBins).filter(b => b[1] > 0); |
| | let distChartHtml = '<div class="bar-chart">'; |
| | if (sortedBins.length > 0) { |
| | const maxCount = Math.max(...sortedBins.map(b => b[1])); |
| | sortedBins.forEach(([label, count]) => { |
| | const barWidth = Math.max((count / maxCount) * 100, 5); |
| | distChartHtml += `<div class="bar-item"><span class="bar-label" title="${label}">${label}</span><div class="bar-wrapper"><div class="bar" style="width: ${barWidth}%; background: var(--gradient-warning); color: var(--text);">${count} کارگاه</div></div></div>`; |
| | }); |
| | } |
| | distChartHtml += '</div>'; |
| | employeeDistributionChart.innerHTML = distChartHtml; |
| | } |
| | |
| | btnRefreshAnalysis.addEventListener('click', () => { |
| | renderAnalysis(); |
| | showMessage('آمار و تحلیلها با موفقیت بروزرسانی شد.', 2000); |
| | }); |
| | |
| | btnShowMap.addEventListener('click', () => { |
| | const inspections = loadInspections(); |
| | const locations = inspections |
| | .filter(insp => insp.gps && insp.gps.includes(',')) |
| | .map(insp => insp.gps.trim().replace(/\s/g, '')); |
| | |
| | if (locations.length === 0) { |
| | alert('هیچ بازرسی با مختصات GPS معتبر برای نمایش روی نقشه یافت نشد.'); |
| | return; |
| | } |
| | |
| | const baseUrl = 'https://www.google.com/maps/dir/'; |
| | const url = baseUrl + locations.join('/'); |
| | window.open(url, '_blank'); |
| | }); |
| | |
| | |
| | function updateSearchInspectorOptions() { |
| | const inspectors = loadInspectors(); |
| | searchInspector.innerHTML = '<option value="">همه بازرسها</option>'; |
| | inspectors.forEach(insp => { |
| | const option = document.createElement('option'); |
| | option.value = insp.personnel; |
| | option.textContent = `${insp.name} ${insp.family} (${insp.personnel})`; |
| | searchInspector.appendChild(option); |
| | }); |
| | } |
| | |
| | function handleSearch() { |
| | const allInspections = loadInspections(); |
| | |
| | const inspectorPersonnel = searchInspector.value; |
| | const workshop = searchWorkshopName.value.trim().toLowerCase(); |
| | const activity = searchActivityField.value.trim().toLowerCase(); |
| | const employer = searchEmployerName.value.trim().toLowerCase(); |
| | const minEmp = parseInt(searchMinEmployees.value) || 0; |
| | const maxEmp = parseInt(searchMaxEmployees.value) || Infinity; |
| | |
| | filteredInspections = allInspections.filter(insp => { |
| | const inspectorMatch = !inspectorPersonnel || insp.personnel === inspectorPersonnel; |
| | const workshopMatch = !workshop || insp.workshopName.toLowerCase().includes(workshop); |
| | const activityMatch = !activity || insp.activityField.toLowerCase().includes(activity); |
| | const employerMatch = !employer || insp.employerName.toLowerCase().includes(employer); |
| | const employeeMatch = insp.numEmployees >= minEmp && insp.numEmployees <= maxEmp; |
| | return inspectorMatch && workshopMatch && activityMatch && employerMatch && employeeMatch; |
| | }); |
| | |
| | renderInspections(filteredInspections, searchResultsTableBody); |
| | searchResultSummary.textContent = `تعداد نتایج یافت شده: ${filteredInspections.length} مورد`; |
| | btnExportFiltered.disabled = filteredInspections.length === 0; |
| | } |
| | |
| | btnRunSearch.addEventListener('click', handleSearch); |
| | btnClearSearch.addEventListener('click', () => { |
| | searchForm.reset(); |
| | filteredInspections = []; |
| | searchResultsTableBody.innerHTML = ''; |
| | searchResultSummary.textContent = 'برای مشاهده نتایج، فیلترها را تنظیم کرده و روی دکمه "اعمال فیلتر" کلیک کنید.'; |
| | btnExportFiltered.disabled = true; |
| | }); |
| | |
| | btnExportFiltered.addEventListener('click', () => { |
| | if (filteredInspections.length > 0) { |
| | |
| | exportToExcel(filteredInspections); |
| | showMessage('خروجی Excel از نتایج فیلتر شده با موفقیت ایجاد شد.', 3000, searchMessageEl); |
| | } |
| | }); |
| | |
| | |
| | btnExportAllData.addEventListener('click', () => { |
| | btnSaveData.click(); |
| | }); |
| | |
| | btnImportData.addEventListener('click', () => { |
| | fileImporter.click(); |
| | }); |
| | |
| | fileImporter.addEventListener('change', (event) => { |
| | const file = event.target.files[0]; |
| | if (!file) return; |
| | |
| | const reader = new FileReader(); |
| | reader.onload = (e) => { |
| | try { |
| | const data = JSON.parse(e.target.result); |
| | if (data && data.inspections && data.inspectors && data.managers) { |
| | if (confirm('آیا مطمئن هستید که میخواهید دادههای فعلی را با اطلاعات فایل پشتیبان جایگزین کنید؟ این عمل غیرقابل بازگشت است.')) { |
| | saveInspections(data.inspections); |
| | saveInspectors(data.inspectors); |
| | saveManagers(data.managers); |
| | alert('دادهها با موفقیت بازیابی شدند.'); |
| | location.reload(); |
| | } |
| | } else { |
| | alert('فایل پشتیبان معتبر نیست. ساختار فایل صحیح نمیباشد.'); |
| | } |
| | } catch (error) { |
| | alert('خطا در خواندن فایل. لطفاً از یک فایل JSON معتبر استفاده کنید.'); |
| | console.error("خطای Import:", error); |
| | } finally { |
| | fileImporter.value = ''; |
| | } |
| | }; |
| | reader.readAsText(file); |
| | }); |
| | |
| | btnClearAllData.addEventListener('click', () => { |
| | if (confirm('هشدار: آیا واقعاً قصد دارید تمام دادههای سامانه را حذف کنید؟')) { |
| | if (confirm('این عمل غیرقابل بازگشت است. برای تایید نهایی، دوباره روی OK کلیک کنید.')) { |
| | localStorage.clear(); |
| | alert('تمام دادهها با موفقیت حذف شدند.'); |
| | location.reload(); |
| | } |
| | } |
| | }); |
| | |
| | |
| | |
| | renderInspectors(); |
| | renderManagers(); |
| | renderInspections(); |
| | renderAnalysis(); |
| | |
| | if (navigator.geolocation) { |
| | btnGetGPS.disabled = false; |
| | } |
| | }); |
| | </script> |
| | </body> |
| | </html> |