Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
HTML
<style>
@font-face {
  font-family: "IRANSans";
  src: url("/download/attachments/129695794/IRANSansXVF.woff2") format("woff2");
}

@font-face {
  font-family: "Vazir";
  src: url("/download/attachments/129695794/Vazir-Regular.woff2") format("woff2");
}

:root {
  --brand: #00bcd4;
  --brand-2: #0097a7;
  --brand-soft: #f0fcfd;
  --brand-border: #b2ebf2;
}

body {
  background: #f4f5f8;
  font-family: Vazir, IRANSans, Tahoma;
}

#public-resolutions-app {
  direction: rtl;
  max-width: 1150px;
  margin: 30px auto;
  background: #fff;
  border: 1px solid #e6e6e6;
  border-radius: 16px;
  padding: 24px;
  box-shadow: 0 4px 14px rgba(0,0,0,0.10);
  font-family: Vazir, IRANSans, Tahoma;
}

.public-header {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 18px;
}

.public-title {
  color: var(--brand);
  font-size: 20px;
  font-weight: 900;
  margin: 0;
}

.public-filters {
  background: var(--brand-soft);
  border: 1px solid var(--brand-border);
  border-radius: 14px;
  padding: 14px;
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}

.public-field {
  flex: 1 1 180px;
}

.public-field label {
  display: block;
  font-weight: 800;
  font-size: 13px;
  margin-bottom: 5px;
}

.public-field input,
.public-field select {
  width: 100%;
  box-sizing: border-box;
  border: 1px solid #d1d5db;
  border-radius: 9px;
  padding: 8px 10px;
  font-family: inherit;
  font-size: 13px;
  background: #fff;
}

.public-field input:focus,
.public-field select:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 5px rgba(0,188,212,0.25);
}

.public-table-wrap {
  width: 100%;
  overflow-x: auto;
  border-radius: 14px;
  border: 1px solid #e5e7eb;
}

#publicTable {
  width: 100%;
  min-width: 850px;
  border-collapse: collapse;
  background: #fff;
  font-size: 13px;
}

#publicTable th {
  background: #f5f6fb;
  color: #1f2933;
  border: 1px solid #e5e7eb;
  padding: 10px;
  text-align: center;
  font-weight: 900;
}

#publicTable td {
  border: 1px solid #e5e7eb;
  padding: 9px 10px;
  text-align: center;
  vertical-align: middle;
}

#publicTable td.title-cell {
  text-align: right;
  line-height: 2;
  white-space: pre-wrap;
}

#publicTable tbody tr:nth-child(even) {
  background: #fafafa;
}

.public-empty,
.public-loading,
.public-error {
  text-align: center;
  padding: 18px;
  border-radius: 12px;
  margin-top: 14px;
  font-weight: 800;
}

.public-loading {
  background: #f8fafc;
  color: #334155;
}

.public-empty {
  background: #fff7edvar(--brand-soft);
  color: var(--brand-2);
  border: #9a3412 1px solid var(--brand-border);
}

.public-error {
  background: #fef2f2;
  color: #b91c1c;
}
</style>

<div id="public-resolutions-app">
  <div class="public-header">
    <h1 class="public-title">📌 مصوبات عمومی غرب</h1>
  </div>

  <div class="public-filters">
    <div class="public-field">
      <label>جستجو در عنوان مصوبه</label>
      <input type="text" id="searchText" placeholder="مثلاً بخشی از عنوان مصوبه">
    </div>

    <div class="public-field">
      <label>کمیته</label>
      <select id="committeeFilter">
        <option value="">همه کمیته‌ها</option>
      </select>
    </div>

    <div class="public-field">
      <label>تاریخ جلسه</label>
      <input type="text" id="dateFilter" placeholder="مثلاً 1405/03/24">
    </div>
  </div>

  <div id="publicStatus" class="public-loading">در حال بارگذاری مصوبات عمومی...</div>

  <div class="public-table-wrap" style="display:none;" id="publicTableWrap">
    <table id="publicTable">
      <thead>
        <tr>
          <th style="width:5%;">ردیف</th>
          <th style="width:18%;">کمیته</th>
          <th style="width:12%;">تاریخ جلسه</th>
          <th style="width:35%;">عنوان مصوبه</th>
          <th style="width:12%;">مسئول پیگیری</th>
          <th style="width:12%;">مسئول اجرا</th>
          <th style="width:10%;">مهلت مقرر</th>
        </tr>
      </thead>
      <tbody id="publicBody"></tbody>
    </table>
  </div>
</div>

<script>
(function () {
  /*
    صفحه مصوبات عمومی غرب:.
    این pageIdعدد باید دقیقاً همان pageIdصفحه‌ای باشد که فرم ثبت صورتجلسه غرب
    public_resolutions.json را روی آن ذخیره می‌کند.
  */
  const PUBLIC_RESOLUTIONS_PAGE_ID = 152371262;
  const PUBLIC_RESOLUTIONS_FILE = "public_resolutions.json";

   const BASE = (() => {
    try {
      if (window.AJS && typeof AJS.contextPath === "function") return AJS.contextPath();
    } catch (e) {}

    const m = location.pathname.match(/^(.+?)\/pages\//);
    return m ? m[1] : "";
  })();

  const baseUrl =
    (window.AJS && window.AJS.params && (window.AJS.params.baseURL || window.AJS.params.baseUrl)) ||
    (location.origin + BASE);

  let allRows = [];

  const statusBox = document.getElementById("publicStatus");
  const tableWrap = document.getElementById("publicTableWrap");
  const tbody = document.getElementById("publicBody");
  const searchText = document.getElementById("searchText");
  const committeeFilter = document.getElementById("committeeFilter");
  const dateFilter = document.getElementById("dateFilter");

  function safeText(v) {
    v = String(v || "");
    var amp = String.fromCharCode(38);

    v = v.replace(/&/g, amp + "amp;");
    v = v.replace(/</g, amp + "lt;");
    v = v.replace(/>/g, amp + "gt;");
    v = v.replace(/"/g, amp + "quot;");
    v = v.replace(/'/g, amp + "#39;");

    return v;
  }

  function normalize(v) {
    return String(v || "")
      .replace(/\u200c/g, "")
      .replace(/\s+/g, " ")
      .trim();
  }

  function toPersianDigits(v) {
    return String(v || "").replace(/\d/g, function (d) {
      return "۰۱۲۳۴۵۶۷۸۹"[d];
    });
  }

  function normalizeResolutionArray(data) {
    if (Array.isArray(data)) return data;
    if (data && Array.isArray(data.items)) return data.items;
    if (data && Array.isArray(data.results)) return data.results;
    if (data && Array.isArray(data.data)) return data.data;
    return [];
  }

  async function loadJSON(pageId, filename) {
    try {
      const directUrl =
        `${BASE}/download/attachments/${pageId}/${encodeURIComponent(filename)}?api=v2&cb=${Date.now()}`;

      const r1 = await fetch(directUrl, {
        credentials: "include",
        headers: { Accept: "application/json,text/plain,*/*" }
      });

      if (r1.ok) {
        const txt = await r1.text();
        if (txt && txt.trim()) return JSON.parse(txt);
      }
    } catch (e1) {
      console.warn("direct loadJSON failed:", e1);
    }

    try {
      const metaUrl =
        `${BASE}/rest/api/content/${pageId}/child/attachment?filename=${encodeURIComponent(filename)}&expand=version`;

      const r2 = await fetch(metaUrl, {
        credentials: "include",
        headers: { Accept: "application/json" }
      });

      if (!r2.ok) return [];

      const meta = await r2.json();
      const att = meta && meta.results && meta.results[0] ? meta.results[0] : null;

      if (!att || !att._links || !att._links.download) return [];

      let dl = att._links.download;
      let downloadUrl = dl.indexOf("http") === 0 ? dl : (baseUrl + dl);
      downloadUrl += (downloadUrl.indexOf("?") >= 0 ? "&" : "?") + "cb=" + Date.now();

      const r3 = await fetch(downloadUrl, {
        credentials: "include",
        headers: { Accept: "application/json,text/plain,*/*" }
      });

      if (!r3.ok) return [];

      const txt = await r3.text();
      if (!txt || !txt.trim()) return [];

      return JSON.parse(txt);
    } catch (e2) {
      console.error("loadJSON error:", e2);
      return [];
    }
  }

  /*
 async function pageExists(pageId) { خروجی:
    pageIdexists = String(pageId || "").trim();

    if (!pageId) return false;

صفحه وجود دارد
    deleted = صفحه حذف شده یا در دسترس نیست به خاطر حذف
    unknown = به خاطر خطای دسترسی/شبکه مطمئن نیستیم، پس رکورد را حذف نمی‌کنیم
  */
  async function getPageState(pageId) {
    pageId = String(pageId || "").trim();

    if (!pageId) return "unknown";

    try {
      const url = `${baseUrl}/rest/api/content/${encodeURIComponent(pageId)}?status=current`;
      const res = await fetch(url, {
        credentials: "include",
        headers: { Accept: "application/json" }
      });

      if (res.ok) return "exists";

      if (res.status === 404 || res.status === 410) return "deleted";

      /*
        اگر 403 باشد، یعنی شاید صفحه وجود دارد ولی کاربر دسترسی ندارد.
        برای اینکه رکورد اشتباهی حذف نشود، نگه می‌داریم.
      */
      if (res.status === 403) return "unknown";

      return "unknown";
    } catch (e) {
      console.warn("getPageState failed:", pageId, e);
      return "unknown";
    }
  }

  async function getAttachmentByName(pageId, filename) {
    try {
      const resurl = await fetch(
        `${baseUrlBASE}/rest/api/content/${encodeURIComponent(pageId)}`api/content/${pageId}/child/attachment?filename=${encodeURIComponent(filename)}&expand=version`;

      const r = await fetch(url, {
        credentials: "include",
        headers: { Accept: "application/json" }
    }
      });
  });

      if (!r.ok) return null;

      const j = await r.json();
      return j.results && j.results[0] ? j.results[0] : null;
    } catch return res.ok;(e) {
    } catch console.error("getAttachmentByName error:", e) {;
      return falsenull;
    }
  }

  async function getAttachmentByNameupdateData(pageId, attachmentId, file, filename) {
    try {
  const fd = new FormData();
    const url =fd.append("file", file, filename);

    const r = await fetch(`${BASE}/rest/api/content/${pageId}/child/attachment?filename=/${encodeURIComponent(filename)}&expand=version`;

attachmentId}/data`, {
      const r = await fetch(url, {method: "POST",
        credentials: "include",
        headers: { Accept: "application/json" }
      });

      if (!r.ok) return null;
  "X-Atlassian-Token": "no-check",
      const j = await r.json();Accept: "application/json"
      return j.results && j.results[0] ? j.results[0] : null;},
      body: fd
    } catch (e) {);

    if  console.error("getAttachmentByName error:", e);(!r.ok) {
      return nullthrow new Error(await r.text());
    }
  }

  async function updateDatauploadNew(pageId, attachmentId, file, filename) {
    const fd = new FormData();
    fd.append("file", file, filename);

    const r = await fetch(`${BASE}/rest/api/content/${pageId}/child/attachment/${attachmentId}/data`attachment`, {
      method: "POST",
      credentials: "include",
      headers: {
        "X-Atlassian-Token": "no-check",
        Accept: "application/json"
      },
      body: fd
    });

    if (!r.ok) {
      throw new Error(await r.text());
    }
  });

  async  functionif uploadNew(pageId, file, filename(!r.ok) {
    const fd =throw new FormDataError(await r.text());
    fd.append("file", file, filename);}
  }

  async  const r = await fetch(`${BASE}/rest/api/content/${pageId}/child/attachment`,function savePublicRows(rows) {
    const blob =  method: "POST",new Blob([JSON.stringify(rows, null, 2)], {
      credentialstype: "includeapplication/json",
      headers: {});

    const existing =  "X-Atlassian-Token": "no-check",
        Accept: "application/json"await getAttachmentByName(PUBLIC_RESOLUTIONS_PAGE_ID, PUBLIC_RESOLUTIONS_FILE);

    if (existing) },{
      body: fd
    }await updateData(PUBLIC_RESOLUTIONS_PAGE_ID, existing.id, blob, PUBLIC_RESOLUTIONS_FILE);

    if (!r.ok)} else {
      throw new Error(await r.text()await uploadNew(PUBLIC_RESOLUTIONS_PAGE_ID, blob, PUBLIC_RESOLUTIONS_FILE);
    }
  }

  async function savePublicRowsremoveDeletedMeetingRows(rows) {
    const blobvalidRows = new Blob([JSON.stringify(rows, null, 2)], {
      type: "application/json"
    });

    const existing = await getAttachmentByName(PUBLIC_RESOLUTIONS_PAGE_ID, PUBLIC_RESOLUTIONS_FILE);

    if (existing) {
[];
    const deletedRows = [];

    for (const r of rows) {
      if (!r || !String(r.title || "").trim()) continue;

      const awaitpageId = updateData(PUBLIC_RESOLUTIONS_PAGE_ID, existing.id, blob, PUBLIC_RESOLUTIONS_FILE);
String(r.pageId || r.minutesPageId || "").trim();

    } else {/*
      await uploadNew(PUBLIC_RESOLUTIONS_PAGE_ID, blob, PUBLIC_RESOLUTIONS_FILE);
    }
  }

  async function removeDeletedMeetingRows(rows) {
    const validRows = [];

    for (const r of rows) {  رکوردهای خیلی قدیمی که pageId ندارند قابل تشخیص نیستند.
        حذفشان نمی‌کنیم تا اطلاعات سالم اشتباهی پاک نشود.
      */
      if (!r || !String(r.title || "").trim()) continue;pageId) {
        validRows.push(r);
        continue;
      }

      const pageIdstate = await StringgetPageState(r.pageId || r.minutesPageId || "").trim();

      if (!pageIdstate === "deleted") {
        validRowsdeletedRows.push(r);
        continue;
      }

      const exists = await pageExists(pageIdvalidRows.push(r);

    }

   if (exists)return {
       validRows: validRows.push(r);,
      }deletedRows: deletedRows
    }

    return validRows;
  }

  function fillCommitteeFilter(rows) {
    const oldValue = committeeFilter.value || "";

    const committees = Array.from(new Set(
      rows.map(r => normalize(r.committee)).filter(Boolean)
    )).sort();

    committeeFilter.innerHTML =
      `<option value="">همه کمیته‌ها</option>` +
      committees.map(c => `<option value="${safeText(c)}">${safeText(c)}</option>`).join("");

    committeeFilter.value = oldValue;
  }

  function getFilteredRows() {
    const q = normalize(searchText.value);
    const c = normalize(committeeFilter.value);
    const d = normalize(dateFilter.value);

    return allRows.filter(function (r) {
      const title = normalize(r.title);
      const committee = normalize(r.committee);
      const meetingDate = normalize(r.meetingDate);

      if (q && !title.includes(q)) return false;
      if (c && committee !== c) return false;
      if (d && !meetingDate.includes(d)) return false;

      return true;
    });
  }

  function render() {
    const rows = getFilteredRows();

    tbody.innerHTML = "";

    if (!rows.length) {
      tableWrap.style.display = "none";
      statusBox.style.display = "block";
      statusBox.className = "public-empty";
      statusBox.textContent = "موردی برای نمایش وجود ندارد.";
      return;
    }

    rows.forEach(function (r, index) {
      const tr = document.createElement("tr");

      tr.innerHTML = `
        <td>${toPersianDigits(index + 1)}</td>
        <td>${safeText(r.committee || "")}</td>
        <td>${safeText(toPersianDigits(r.meetingDate || ""))}</td>
        <td class="title-cell">${safeText(r.title || "")}</td>
        <td>${safeText(r.followup || "")}</td>
        <td>${safeText(r.executor || "")}</td>
        <td>${safeText(toPersianDigits(r.deadline || ""))}</td>
      `;

      tbody.appendChild(tr);
    });

    statusBox.style.display = "none";
    tableWrap.style.display = "block";
  }

  async function loadPublicResolutions() {
    statusBox.style.display = "block";
    statusBox.className = "public-loading";
    statusBox.textContent = "در حال بارگذاری مصوبات عمومی...";
    tableWrap.style.display = "none";

    try {
      const data = await loadJSON(PUBLIC_RESOLUTIONS_PAGE_ID, PUBLIC_RESOLUTIONS_FILE);
      const rows = normalizeResolutionArray(data);

      const validRowscleanupResult = await removeDeletedMeetingRows(rows);
      const validRows = cleanupResult.validRows;
      const deletedRows = cleanupResult.deletedRows;

      allRows = validRows;

      /*
        اگر صفحه صورتجلسه حذف شده باشد، اینرکورد بخشاز تلاشفایل می‌کندJSON رکوردهای مرده را از JSON حذف کندهم پاک می‌شود.
        اگر کاربر فعلی دسترسی ویرایش attachmentاتچمنت نداشته باشد، نمایش جدول خراب نمی‌شود.
      */
      if (deletedRows.length > 0 || validRows.length !== rows.length) {
        try {
          await savePublicRows(validRows);
          console.log("✅ رکوردهای صورتجلسه‌های حذف‌شده از مصوبات عمومی پاک شد:", deletedRows.length);
        } catch (cleanupError) {
          console.warn("cleanup⚠️ حذف از فایل public_resolutions.json skipped انجام نشد، ولی از نمایش حذف شد:", cleanupError);
        }
      }

      fillCommitteeFilter(allRows);
      render();

    } catch (e) {
      console.error(e);
      statusBox.style.display = "block";
      statusBox.className = "public-error";
      statusBox.textContent = "خطا در بارگذاری مصوبات عمومی.";
      tableWrap.style.display = "none";
    }
  }

  searchText.addEventListener("input", render);
  committeeFilter.addEventListener("change", render);
  dateFilter.addEventListener("input", render);

  loadPublicResolutions();
})();
</script>

...