ashkanpourali's picture
Upload Gens3.html
a877646 verified
raw
history blame
116 kB
<!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/[email protected]/dist/jalaali.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]/openlocationcode.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
// جلوگیری از submit شدن فرم‌ها و رفرش شدن صفحه
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); });
});
// storage helpers (بدون تغییر)
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')}`;
}
// تابع تبدیل مختصات GPS به فرمت Plus Code
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; // اگر مقادیر عددی نبودند، همان مقدار اصلی را برگردان
}
// تبدیل به Plus Code با استفاده از کتابخانه openlocationcode
const plusCode = OpenLocationCode.encode(lat, lon, 10); // 10 برای کد کوتاه
return plusCode;
} catch (error) {
console.error('خطا در تبدیل به Plus Code:', error);
return gpsString; // در صورت خطا، مقدار اصلی را برگردان
}
}
// states
let editingInspectorIndex = -1;
let editingInspectionIndex = -1;
let editingManagerIndex = -1;
let currentInspector = null;
let filteredInspections = []; // 🌟 State برای نتایج جستجو
// render inspectors (بدون تغییر)
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; }
}));
}
// render managers
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;
}
// add/update inspector (بدون تغییر)
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; });
// add/update manager
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);
}
}
});
// دریافت مختصات GPS
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 => {
// اگر GPS در دسترس نبود، از مختصات تقریبی استفاده کن
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()), // اضافه کردن فیلد Plus Code
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;
}
});
});
// 🌟 اصلاح شده: تابع خروجی Excel با هایپرلینک
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; // +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 با موفقیت ایجاد شد.');
}
// 🌟 اصلاح شده: تابع خروجی Word با هایپرلینک
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 با موفقیت ایجاد شد.');
}
// 🌟 اصلاح شده: تابع خروجی PDF (چاپی) با هایپرلینک
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('گزارش چاپی در پنجره جدید باز شد.');
}
// 🌟 اصلاح شده: تابع خروجی Text با لینک کامل نقشه
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>