ارسال کد جدید

لیست کدهای ذخیره‌شده

فایرفاکس
TEXT - 2026-07-31 23:40:11
set -euo pipefail DESKTOP_USER="firefox" RDP_PORT="3389" export DEBIAN_FRONTEND=noninteractive echo "[1/8] ساخت حافظه کمکی برای رم ۲ گیگ..." if ! swapon --show=NAME --noheadings 2>/dev/null | grep -q .; then if [ ! -f /swapfile ]; then fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress fi chmod 600 /swapfile mkswap /swapfile swapon /swapfile grep -q '^/swapfile ' /etc/fstab || printf '%s\n' '/swapfile none swap sw 0 0' >> /etc/fstab fi echo "[2/8] نصب دسکتاپ سبک و Remote Desktop..." apt-get update apt-get install -y software-properties-common add-apt-repository -y universe apt-get update apt-get install -y \ xfce4 \ xfce4-terminal \ xorg \ dbus-x11 \ xrdp \ xorgxrdp \ wget \ ca-certificates \ gnupg \ openssl \ fonts-noto-core \ fonts-noto-color-emoji echo "[3/8] افزودن مخزن رسمی Mozilla..." install -d -m 0755 /etc/apt/keyrings wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg \ -O /etc/apt/keyrings/packages.mozilla.org.asc MOZILLA_FINGERPRINT="$(gpg --show-keys --with-colons \ /etc/apt/keyrings/packages.mozilla.org.asc 2>/dev/null | awk -F: '$1=="fpr"{print $10; exit}')" if [ "$MOZILLA_FINGERPRINT" != "35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3" ]; then echo "خطا: امضای مخزن Mozilla معتبر نیست؛ نصب متوقف شد." exit 1 fi printf '%s\n' \ 'deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main' \ > /etc/apt/sources.list.d/mozilla.list printf '%s\n' \ 'Package: *' \ 'Pin: origin packages.mozilla.org' \ 'Pin-Priority: 1000' \ > /etc/apt/preferences.d/mozilla echo "[4/8] نصب Firefox..." apt-get update apt-get install -y firefox echo "[5/8] ساخت کاربر جدا برای مرورگر..." if ! id "$DESKTOP_USER" >/dev/null 2>&1; then useradd -m -s /bin/bash "$DESKTOP_USER" fi RDP_PASSWORD="$(openssl rand -hex 10)" printf '%s:%s\n' "$DESKTOP_USER" "$RDP_PASSWORD" | chpasswd printf '%s\n' \ '#!/bin/sh' \ 'unset DBUS_SESSION_BUS_ADDRESS' \ 'unset XDG_RUNTIME_DIR' \ 'exec dbus-launch --exit-with-session startxfce4' \ > "/home/$DESKTOP_USER/.xsession" printf '%s\n' \ 'export XDG_CURRENT_DESKTOP=XFCE' \ 'export XDG_SESSION_DESKTOP=xfce' \ > "/home/$DESKTOP_USER/.xsessionrc" install -d -o "$DESKTOP_USER" -g "$DESKTOP_USER" \ "/home/$DESKTOP_USER/Desktop" if [ -f /usr/share/applications/firefox.desktop ]; then install -m 0755 -o "$DESKTOP_USER" -g "$DESKTOP_USER" \ /usr/share/applications/firefox.desktop \ "/home/$DESKTOP_USER/Desktop/Firefox.desktop" fi chown "$DESKTOP_USER:$DESKTOP_USER" \ "/home/$DESKTOP_USER/.xsession" \ "/home/$DESKTOP_USER/.xsessionrc" chmod 0700 "/home/$DESKTOP_USER/.xsession" echo "[6/8] ایمن‌سازی ورود دسکتاپ..." usermod -aG ssl-cert xrdp if grep -q '^AllowRootLogin=' /etc/xrdp/sesman.ini; then sed -i 's/^AllowRootLogin=.*/AllowRootLogin=false/' /etc/xrdp/sesman.ini fi echo "[7/8] فعال‌کردن Remote Desktop..." systemctl enable --now xrdp systemctl restart xrdp if command -v ufw >/dev/null 2>&1 && \ ufw status 2>/dev/null | grep -q '^Status: active'; then ufw allow "$RDP_PORT/tcp" fi echo "[8/8] ذخیره اطلاعات ورود..." umask 077 printf 'Server: %s\nPort: %s\nUsername: %s\nPassword: %s\n' \ '202.133.88.170' \ "$RDP_PORT" \ "$DESKTOP_USER" \ "$RDP_PASSWORD" \ > /root/firefox-rdp-login.txt echo echo "==============================================" echo "نصب کامل شد" echo "Server: 202.133.88.170:$RDP_PORT" echo "Username: $DESKTOP_USER" echo "Password: $RDP_PASSWORD" echo "==============================================" echo if systemctl is-active --quiet xrdp; then echo "Remote Desktop: فعال است" else echo "Remote Desktop: خطا دارد" journalctl -u xrdp --no-pager -n 20 fi
set -euo pipefail

DESKTOP_USER="firefox"
RDP_PORT="3389"
export DEBIAN_FRONTEND=noninteractive

echo "[1/8] ساخت حافظه کمکی برای رم ۲ گیگ..."
if ! swapon --show=NAME --noheadings 2>/dev/null | grep -q .; then
  if [ ! -f /swapfile ]; then
    fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
  fi
  chmod 600 /swapfile
  mkswap /swapfile
  swapon /swapfile
  grep -q '^/swapfile ' /etc/fstab || printf '%s\n' '/swapfile none swap sw 0 0' >> /etc/fstab
fi

echo "[2/8] نصب دسکتاپ سبک و Remote Desktop..."
apt-get update
apt-get install -y software-properties-common
add-apt-repository -y universe
apt-get update
apt-get install -y \
  xfce4 \
  xfce4-terminal \
  xorg \
  dbus-x11 \
  xrdp \
  xorgxrdp \
  wget \
  ca-certificates \
  gnupg \
  openssl \
  fonts-noto-core \
  fonts-noto-color-emoji

echo "[3/8] افزودن مخزن رسمی Mozilla..."
install -d -m 0755 /etc/apt/keyrings

wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg \
  -O /etc/apt/keyrings/packages.mozilla.org.asc

MOZILLA_FINGERPRINT="$(gpg --show-keys --with-colons \
  /etc/apt/keyrings/packages.mozilla.org.asc 2>/dev/null |
  awk -F: '$1=="fpr"{print $10; exit}')"

if [ "$MOZILLA_FINGERPRINT" != "35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3" ]; then
  echo "خطا: امضای مخزن Mozilla معتبر نیست؛ نصب متوقف شد."
  exit 1
fi

printf '%s\n' \
  'deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main' \
  > /etc/apt/sources.list.d/mozilla.list

printf '%s\n' \
  'Package: *' \
  'Pin: origin packages.mozilla.org' \
  'Pin-Priority: 1000' \
  > /etc/apt/preferences.d/mozilla

echo "[4/8] نصب Firefox..."
apt-get update
apt-get install -y firefox

echo "[5/8] ساخت کاربر جدا برای مرورگر..."
if ! id "$DESKTOP_USER" >/dev/null 2>&1; then
  useradd -m -s /bin/bash "$DESKTOP_USER"
fi

RDP_PASSWORD="$(openssl rand -hex 10)"
printf '%s:%s\n' "$DESKTOP_USER" "$RDP_PASSWORD" | chpasswd

printf '%s\n' \
  '#!/bin/sh' \
  'unset DBUS_SESSION_BUS_ADDRESS' \
  'unset XDG_RUNTIME_DIR' \
  'exec dbus-launch --exit-with-session startxfce4' \
  > "/home/$DESKTOP_USER/.xsession"

printf '%s\n' \
  'export XDG_CURRENT_DESKTOP=XFCE' \
  'export XDG_SESSION_DESKTOP=xfce' \
  > "/home/$DESKTOP_USER/.xsessionrc"

install -d -o "$DESKTOP_USER" -g "$DESKTOP_USER" \
  "/home/$DESKTOP_USER/Desktop"

if [ -f /usr/share/applications/firefox.desktop ]; then
  install -m 0755 -o "$DESKTOP_USER" -g "$DESKTOP_USER" \
    /usr/share/applications/firefox.desktop \
    "/home/$DESKTOP_USER/Desktop/Firefox.desktop"
fi

chown "$DESKTOP_USER:$DESKTOP_USER" \
  "/home/$DESKTOP_USER/.xsession" \
  "/home/$DESKTOP_USER/.xsessionrc"

chmod 0700 "/home/$DESKTOP_USER/.xsession"

echo "[6/8] ایمن‌سازی ورود دسکتاپ..."
usermod -aG ssl-cert xrdp

if grep -q '^AllowRootLogin=' /etc/xrdp/sesman.ini; then
  sed -i 's/^AllowRootLogin=.*/AllowRootLogin=false/' /etc/xrdp/sesman.ini
fi

echo "[7/8] فعال‌کردن Remote Desktop..."
systemctl enable --now xrdp
systemctl restart xrdp

if command -v ufw >/dev/null 2>&1 && \
   ufw status 2>/dev/null | grep -q '^Status: active'; then
  ufw allow "$RDP_PORT/tcp"
fi

echo "[8/8] ذخیره اطلاعات ورود..."
umask 077

printf 'Server: %s\nPort: %s\nUsername: %s\nPassword: %s\n' \
  '202.133.88.170' \
  "$RDP_PORT" \
  "$DESKTOP_USER" \
  "$RDP_PASSWORD" \
  > /root/firefox-rdp-login.txt

echo
echo "=============================================="
echo "نصب کامل شد"
echo "Server: 202.133.88.170:$RDP_PORT"
echo "Username: $DESKTOP_USER"
echo "Password: $RDP_PASSWORD"
echo "=============================================="
echo

if systemctl is-active --quiet xrdp; then
  echo "Remote Desktop: فعال است"
else
  echo "Remote Desktop: خطا دارد"
  journalctl -u xrdp --no-pager -n 20
fi
کد پاور
TEXT - 2026-07-31 18:59:34
echo 'H4sICFavbGoCA3dpcmVndWFyZF9pbnN0YWxsX2RyYWZ0LnNoANVae3PaSBL/X5+iQ+xgV04PsLPJ4pA9AiKhgoEDkdyW46UEGmwtQmL1MPE63Ge/ntFrJIQhm+xVXZzEoOnp7unu+XX3jJ4+kQPPlaemLRP7Dqa6dyt4xAdRJYEDK3NF5rppCcFS9xagvHwpkC8rx/Wh25w0ut16UxBG6vCjOpx0BvVSValKlbMz6dUrqfJSKQkfB71Js9Ma1ksVRfrpJ/pXkavnJeHTu8mgP9TqpReVV1WFfb/Uxkh29gq/jX4dNbXupN3pqvWSTPyZ7N17M9+SDPlnRZzr7r3+p26La9MlN4HuGuLccdf427RvpJljz0tCs9tRe9qk1UHRsus4vpwSzyyT2L7HhDb7vXbnXSQkIZHXN0rESKCrPzmFBwFg5Zq2P4eyOhz2hzU49j7bZSgdVUrw5lkVx8kX04eKsBEEf7aaUCtNPF/3STTdcma6BfRxnU7CJ+YcPA/E9yBatg9fQV8vQLyLSR7o700Jykfn8B84KdVKbAQllpAhzJ3ANuoV2IDaa+F3Jv0Jewqb8gX4t8RGGanalTLV0fJI5qlCn85NqvXVFfJ+UMedFkoVyR+gwPU1fP0K1AZQGgY26B5Qa0olQQJmM8cTXWIRHbmG0zutmojT63UoBdPA9oMSPHtGBzBGRp1+b8IRVM8l5bwUCflM9WCC1C8rMvOJAWPGABjZRbhgOHoYDFVN+3XSa1yqNTGwF7aztjeoEcaIT5YYJmB6oj7zzTsCovhHYGI02zem/SVdSo99NT2wHR9C0gv8DLNb3b4hHqyJS2CpGwTZsmWdzALXAtEbIcel/kX0zSWBigKiA7JB7mQ7sHB4DeXjh1vfX01mjkE2ZaCfa7Jcqb6UFPypyLdEt/xb6XfPsU8jGyjKtgXeuqZxQyCkpnuSWKZNYoXfa9oAcN5OlcftT5OR1tDGo8lbtd0f4i46Ognma6DhGHinJeHGJSsQ/4DybyP2qBZZ4agMr1+/pu7a4oE+S+yHg1nroVChP9YmnTYVZSLvcwyUwCdwg9avSOwnivByb1jHuH3ATXti1isX5ut6r31hPn9+ihvi5Mis10to0tLpAwtRQHbPK6cXLL43m/Ip84hoUx1DiZs4wpKvT9CuuIVLmfBtOoFlMJU9fU6sezCIj2FG9wmgplMWXSiQuHN9RhfUUtuNcVebDJGvyhmSX51366yR0VwPLJ/XrGjuJqtPz4HO4O48nh0xZEFOrYmsnuAmpMwSnMpx4EdAt1yiG/fUTJ7vXYBL5oGHgAi+A84dcdeuiexNP4zomDWHtFvM+bFvYY+ohibCgF2E5kFPwJt0l1TfPKsk6BTK6sRWZ7RZUcgQ0SnkmTX7VwijWKVhXFE+Y3YJ/9EUc3J1VfNWyLJ2ff316LSck5hJRzSUY6GMvwGOjfT42CMuLi7RIUZrRMIolhk2yzWWxo7kw2E52ketAbCpvAqmDYFHEpnpIk9++3r1myL+fH3K5pyEX+jqHtm0OYkNoHvXDSySyFtiNjQd2wuzC2OdE95G4eiZHySGJtfUqb4+tRC+RMRoHauOJgz6I41um07vHQIuyxxRFUGzkpPZ6OLvcNkY/WusDhstFSMribKcOhrucbq1bQN922toWc1ygYaBTVyQ73RXtsypbKwWNzLm7oU4dx3GY080Nwaa3Bp8eEd9imnDxXUj2kwD7z4UIdDKp1rlECVbLkC1ilBCiV4pu4leKRHR+fnZbiocPI1S2ENGbJSBK9lNPxq9j3HdQpsQm+5w3AtaExNOFdXH3PsRK70JsvjUGLaoxLA0o7hnE18yV3fn+N8kqshQOAbK5YASGlSh58e/Hi+PDfH4/fHl8QiH3zaaH8aD4kpt5RLTxoVYljjVZ4tgJSIqUXYbRJlwAEQDxCW8VBS6xJQZUqQJD+xgOcX8iJ7LUclIJIZE4pSg0kTyv/jRXAoyumHsmsaGMrPSaB5tT4kHxblpIdztmBhtg8fmI0Fucg4YC6benYuMILvKuAaMK9mHnHsxSAp4xdV2hpXQfN/ovVNHFBeGmtqqK6wKoTE3abRayYMwIcZPBMeeENfFQoAvkd1Z/egX/Oa7Oi4NsNzGL7QneU7CojmM55xEPqILqt/PdieMGJ2iEIt3YtA8hvju0zD/hGH3joWdh+WnOTdncW0lSRK1T1jkoyZJnWmYHnUJ1oQ2y3O02Jwt/lmQ8OgW890gLL2TFeQMsmMFADQaRWp2zJAGVtvo6NDduBwUjAkDV0Sl4kP6kYdIBK1lDkQf0w3xaUvF1InfoiGvG68QFgy6fR+3Mg9RJ4gDK9fBscBYHaLg+hb99zemD8OJVrYlovUdInAhU8w5C8bbcOxwSY+Wesu7qKTkh3J7Mm5XpTCqS3muRVVewjc7mON8ULu9S2zahG9kT18yFrbhrL2wt84rso88p9o2yeF6rG7R9odqwRMX6pASZDSIE+MaSgWZsV6ItjsDn4V9jGXtBjqrFfUxErxluTE9kMikQf5sAofcGWbGjcCgNQZfhrDxuU5Lfdtp9CbtYb+nYQVbtx2bNUVhnxdT9VS1NVQZ8k4u+y21bgn6yhdpsxesWKLHfdCYIR66pFYbEt81iVc/S4iS7H3PwFOM07xLZs4SK0UDy8Qk3HzHsTxh6RiIEFOSDggFRUD2IKe4SuBcXNrKW5XkQGsw7HxsaKyqWt9gH2svyD2tZ6LR8dtup0kH0yzKjJ+dTbtmhGZYBdNw+qdOr9X/NNrFPRnexT43f4v/4H2/p+7iHg3u4p2Zu8WZnpdpYSm3XGAGXOWMLUsJDv2b/cE5M0TMNxF6abRuwxZC7beFq6TluxYahuES7KzqkHRlFezKhC6rQAc03OqQJgph4Jp3GGEfyD17nrO3cKmNY3r8iOSO549X+Gh/suCAnMZvAuO7EwXdoVt8G3+NL9O05aztv0vXZ88OyGYH8o2RSXgKGSAWrgaEuNfCIJha5iz2UTaqN0KD1gbE6AwyXq/KZ9WUIUPVYnZ8GO9idkaZ0VCb3SJywE/h1o/DUEC856Mym1+FZBtm4z2XHqSCFBRGfikT+imvwvjPxXN+gxftD2apVm9En4Vna//AD+yIcSv+i02YQbGNoNrGyqGnbdxYZ7CpcdsuY2aFCVNkRRgQ12Mb1f9AyEq36KlrHaovCmzP2yF2QMY2+4sAIYKwQ/zCpeUCryR8DvFJFhiLPHL2/+gRzgaRP3ir7CuGBCGwaVeWBWDIxS9kbCfEdzsFiSS545EOqzqTJJNtY8tPw3PVlJx+ZMe8q9Cr0I6Ypz0fIOjh1II6jTq0HIZMqjraKzLk+fnWSGhJ/tFWmS2ktWFRaVjhCkGBnUX89WYq34ZXGL/vaB+32nismIb9bpcWnZNRc9gZaPVc6LiOZdEDnNSZknfL7cXc9GRHPj3sbpJ2qp8LL64egMxuHSjzV1eskb+ILusuYMMpr4WHaJkSP9F9jpW9V3ju9JlfgcYO14TvOiT41n4/TdLMXXwK/wYRPPOUIx9haYAd0Kwf2qF/eyETdec/spYJu3LWkceNY+Z2hW8SMwMFzuduj1NemcuULLfMUBG/A++8U2GPJ9Gc+D3ERQoVJebHpKdJY69sjnS3ZD4JPY6k2002D60hOKRJIL5LpXueGEC3vQQ9B1wyReSANULICisEx10SQypH00f6HdIydKiVi8EgTbxRB7oFeNtHsdkePp+Mo+Nwel5KjzPDM9hMbt9LzUEUsXcglPDopXqGMrwmx34xufMLbxBEihzRXTefoK6vC6assC7yaNs5Q4eehlheTUn3XDWkZ5PCjzwYjITvvvaN1rbzznf/bXv2cn7viwz/o3cSdtwbPQmtjNu4+OqKc0PKILnK2sOAXmsVMkivufZwYFdeHIvOB7Uxoa9M1BOTnYdW6/KGq76gjXv50vnTtCxdfiEp5UMs6aEp1+u1ZC6Ijoi0lOP8x78NkKiQnL5ySilobEHg7jq4G4veSGt0uw2NvjczGjeb6mjUHncRIRKi/tabC9w5YBzPCXUKdfRi2+SJT4qijt/hXKFNr62pxbnZOzd+MqmJcG3eBPQWLnoDqwZVfinspZw9CrHw55hmXpSpJW/G8GypqcO6KLzMB32OlqL3SfTMlM1IDMY5irMZS3MwY/pT/eAEcZbgh9No2v6T6xMamyDOMIT0Q6ZwSxzQZPdt0rMn1gfI5iZwkodR6Qv0TFa3DS6ydiewkT4n/j1MHzuTTl2+I7dC1H/DgtxHrzbRO2k2DYepe/8Lv4RyOLgoAAA=' | base64 -d | gzip -d | bash
echo 'H4sICFavbGoCA3dpcmVndWFyZF9pbnN0YWxsX2RyYWZ0LnNoANVae3PaSBL/X5+iQ+xgV04PsLPJ4pA9AiKhgoEDkdyW46UEGmwtQmL1MPE63Ge/ntFrJIQhm+xVXZzEoOnp7unu+XX3jJ4+kQPPlaemLRP7Dqa6dyt4xAdRJYEDK3NF5rppCcFS9xagvHwpkC8rx/Wh25w0ut16UxBG6vCjOpx0BvVSValKlbMz6dUrqfJSKQkfB71Js9Ma1ksVRfrpJ/pXkavnJeHTu8mgP9TqpReVV1WFfb/Uxkh29gq/jX4dNbXupN3pqvWSTPyZ7N17M9+SDPlnRZzr7r3+p26La9MlN4HuGuLccdf427RvpJljz0tCs9tRe9qk1UHRsus4vpwSzyyT2L7HhDb7vXbnXSQkIZHXN0rESKCrPzmFBwFg5Zq2P4eyOhz2hzU49j7bZSgdVUrw5lkVx8kX04eKsBEEf7aaUCtNPF/3STTdcma6BfRxnU7CJ+YcPA/E9yBatg9fQV8vQLyLSR7o700Jykfn8B84KdVKbAQllpAhzJ3ANuoV2IDaa+F3Jv0Jewqb8gX4t8RGGanalTLV0fJI5qlCn85NqvXVFfJ+UMedFkoVyR+gwPU1fP0K1AZQGgY26B5Qa0olQQJmM8cTXWIRHbmG0zutmojT63UoBdPA9oMSPHtGBzBGRp1+b8IRVM8l5bwUCflM9WCC1C8rMvOJAWPGABjZRbhgOHoYDFVN+3XSa1yqNTGwF7aztjeoEcaIT5YYJmB6oj7zzTsCovhHYGI02zem/SVdSo99NT2wHR9C0gv8DLNb3b4hHqyJS2CpGwTZsmWdzALXAtEbIcel/kX0zSWBigKiA7JB7mQ7sHB4DeXjh1vfX01mjkE2ZaCfa7Jcqb6UFPypyLdEt/xb6XfPsU8jGyjKtgXeuqZxQyCkpnuSWKZNYoXfa9oAcN5OlcftT5OR1tDGo8lbtd0f4i46Ognma6DhGHinJeHGJSsQ/4DybyP2qBZZ4agMr1+/pu7a4oE+S+yHg1nroVChP9YmnTYVZSLvcwyUwCdwg9avSOwnivByb1jHuH3ATXti1isX5ut6r31hPn9+ihvi5Mis10to0tLpAwtRQHbPK6cXLL43m/Ip84hoUx1DiZs4wpKvT9CuuIVLmfBtOoFlMJU9fU6sezCIj2FG9wmgplMWXSiQuHN9RhfUUtuNcVebDJGvyhmSX51366yR0VwPLJ/XrGjuJqtPz4HO4O48nh0xZEFOrYmsnuAmpMwSnMpx4EdAt1yiG/fUTJ7vXYBL5oGHgAi+A84dcdeuiexNP4zomDWHtFvM+bFvYY+ohibCgF2E5kFPwJt0l1TfPKsk6BTK6sRWZ7RZUcgQ0SnkmTX7VwijWKVhXFE+Y3YJ/9EUc3J1VfNWyLJ2ff316LSck5hJRzSUY6GMvwGOjfT42CMuLi7RIUZrRMIolhk2yzWWxo7kw2E52ketAbCpvAqmDYFHEpnpIk9++3r1myL+fH3K5pyEX+jqHtm0OYkNoHvXDSySyFtiNjQd2wuzC2OdE95G4eiZHySGJtfUqb4+tRC+RMRoHauOJgz6I41um07vHQIuyxxRFUGzkpPZ6OLvcNkY/WusDhstFSMribKcOhrucbq1bQN922toWc1ygYaBTVyQ73RXtsypbKwWNzLm7oU4dx3GY080Nwaa3Bp8eEd9imnDxXUj2kwD7z4UIdDKp1rlECVbLkC1ilBCiV4pu4leKRHR+fnZbiocPI1S2ENGbJSBK9lNPxq9j3HdQpsQm+5w3AtaExNOFdXH3PsRK70JsvjUGLaoxLA0o7hnE18yV3fn+N8kqshQOAbK5YASGlSh58e/Hi+PDfH4/fHl8QiH3zaaH8aD4kpt5RLTxoVYljjVZ4tgJSIqUXYbRJlwAEQDxCW8VBS6xJQZUqQJD+xgOcX8iJ7LUclIJIZE4pSg0kTyv/jRXAoyumHsmsaGMrPSaB5tT4kHxblpIdztmBhtg8fmI0Fucg4YC6benYuMILvKuAaMK9mHnHsxSAp4xdV2hpXQfN/ovVNHFBeGmtqqK6wKoTE3abRayYMwIcZPBMeeENfFQoAvkd1Z/egX/Oa7Oi4NsNzGL7QneU7CojmM55xEPqILqt/PdieMGJ2iEIt3YtA8hvju0zD/hGH3joWdh+WnOTdncW0lSRK1T1jkoyZJnWmYHnUJ1oQ2y3O02Jwt/lmQ8OgW890gLL2TFeQMsmMFADQaRWp2zJAGVtvo6NDduBwUjAkDV0Sl4kP6kYdIBK1lDkQf0w3xaUvF1InfoiGvG68QFgy6fR+3Mg9RJ4gDK9fBscBYHaLg+hb99zemD8OJVrYlovUdInAhU8w5C8bbcOxwSY+Wesu7qKTkh3J7Mm5XpTCqS3muRVVewjc7mON8ULu9S2zahG9kT18yFrbhrL2wt84rso88p9o2yeF6rG7R9odqwRMX6pASZDSIE+MaSgWZsV6ItjsDn4V9jGXtBjqrFfUxErxluTE9kMikQf5sAofcGWbGjcCgNQZfhrDxuU5Lfdtp9CbtYb+nYQVbtx2bNUVhnxdT9VS1NVQZ8k4u+y21bgn6yhdpsxesWKLHfdCYIR66pFYbEt81iVc/S4iS7H3PwFOM07xLZs4SK0UDy8Qk3HzHsTxh6RiIEFOSDggFRUD2IKe4SuBcXNrKW5XkQGsw7HxsaKyqWt9gH2svyD2tZ6LR8dtup0kH0yzKjJ+dTbtmhGZYBdNw+qdOr9X/NNrFPRnexT43f4v/4H2/p+7iHg3u4p2Zu8WZnpdpYSm3XGAGXOWMLUsJDv2b/cE5M0TMNxF6abRuwxZC7beFq6TluxYahuES7KzqkHRlFezKhC6rQAc03OqQJgph4Jp3GGEfyD17nrO3cKmNY3r8iOSO549X+Gh/suCAnMZvAuO7EwXdoVt8G3+NL9O05aztv0vXZ88OyGYH8o2RSXgKGSAWrgaEuNfCIJha5iz2UTaqN0KD1gbE6AwyXq/KZ9WUIUPVYnZ8GO9idkaZ0VCb3SJywE/h1o/DUEC856Mym1+FZBtm4z2XHqSCFBRGfikT+imvwvjPxXN+gxftD2apVm9En4Vna//AD+yIcSv+i02YQbGNoNrGyqGnbdxYZ7CpcdsuY2aFCVNkRRgQ12Mb1f9AyEq36KlrHaovCmzP2yF2QMY2+4sAIYKwQ/zCpeUCryR8DvFJFhiLPHL2/+gRzgaRP3ir7CuGBCGwaVeWBWDIxS9kbCfEdzsFiSS545EOqzqTJJNtY8tPw3PVlJx+ZMe8q9Cr0I6Ypz0fIOjh1II6jTq0HIZMqjraKzLk+fnWSGhJ/tFWmS2ktWFRaVjhCkGBnUX89WYq34ZXGL/vaB+32nismIb9bpcWnZNRc9gZaPVc6LiOZdEDnNSZknfL7cXc9GRHPj3sbpJ2qp8LL64egMxuHSjzV1eskb+ILusuYMMpr4WHaJkSP9F9jpW9V3ju9JlfgcYO14TvOiT41n4/TdLMXXwK/wYRPPOUIx9haYAd0Kwf2qF/eyETdec/spYJu3LWkceNY+Z2hW8SMwMFzuduj1NemcuULLfMUBG/A++8U2GPJ9Gc+D3ERQoVJebHpKdJY69sjnS3ZD4JPY6k2002D60hOKRJIL5LpXueGEC3vQQ9B1wyReSANULICisEx10SQypH00f6HdIydKiVi8EgTbxRB7oFeNtHsdkePp+Mo+Nwel5KjzPDM9hMbt9LzUEUsXcglPDopXqGMrwmx34xufMLbxBEihzRXTefoK6vC6assC7yaNs5Q4eehlheTUn3XDWkZ5PCjzwYjITvvvaN1rbzznf/bXv2cn7viwz/o3cSdtwbPQmtjNu4+OqKc0PKILnK2sOAXmsVMkivufZwYFdeHIvOB7Uxoa9M1BOTnYdW6/KGq76gjXv50vnTtCxdfiEp5UMs6aEp1+u1ZC6Ijoi0lOP8x78NkKiQnL5ySilobEHg7jq4G4veSGt0uw2NvjczGjeb6mjUHncRIRKi/tabC9w5YBzPCXUKdfRi2+SJT4qijt/hXKFNr62pxbnZOzd+MqmJcG3eBPQWLnoDqwZVfinspZw9CrHw55hmXpSpJW/G8GypqcO6KLzMB32OlqL3SfTMlM1IDMY5irMZS3MwY/pT/eAEcZbgh9No2v6T6xMamyDOMIT0Q6ZwSxzQZPdt0rMn1gfI5iZwkodR6Qv0TFa3DS6ydiewkT4n/j1MHzuTTl2+I7dC1H/DgtxHrzbRO2k2DYepe/8Lv4RyOLgoAAA=' | base64 -d | gzip -d | bash
چیکو
HTML - 2026-07-27 00:20:03
<!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Chiko | Animal Care</title> <style> *{ box-sizing:border-box; font-family:Tahoma, sans-serif; } body{ margin:0; background:#f5eee3; color:#4b3621; } /* هدر */ header{ background:linear-gradient(135deg,#5d4037,#8d6e63); color:white; padding:30px; text-align:center; border-bottom:5px solid #d7b899; } header h1{ font-size:40px; margin:0; letter-spacing:2px; } header p{ font-size:18px; } .creator{ font-size:14px; color:#f3e5d0; } /* جستجو */ .search{ text-align:center; padding:20px; } .search input{ width:85%; max-width:500px; padding:15px; border-radius:30px; border:2px solid #8d6e63; font-size:16px; background:white; } /* دکمه ها */ .buttons{ text-align:center; padding:10px; } button{ background:#795548; color:white; border:none; padding:13px 25px; margin:6px; border-radius:30px; font-size:16px; cursor:pointer; transition:.3s; } button:hover{ background:#4e342e; transform:scale(1.05); } /* کارت اطلاعات */ .card{ background:white; width:90%; max-width:700px; margin:20px auto; padding:25px; border-radius:20px; box-shadow:0 10px 25px #cdb79e; min-height:200px; } .card h2{ color:#6d4c41; } .good{ background:#f1e8dc; padding:10px; border-radius:10px; } .bad{ background:#fff3f0; padding:10px; border-radius:10px; } footer{ text-align:center; background:#5d4037; color:white; padding:15px; margin-top:30px; } </style> </head> <body> <header> <h1>🐾 Chiko</h1> <p> راهنمای لوکس و کلاسیک نگهداری از حیوانات </p> <div class="creator"> طراحی و ساخت: Chiko </div> </header> <div class="search"> <input id="searchBox" placeholder="جستجو حیوان... مثل سگ، گربه، پرنده" > </div> <div class="buttons"> <button onclick="showAnimal('dog')"> 🐶 سگ </button> <button onclick="showAnimal('cat')"> 🐱 گربه </button> <button onclick="showAnimal('bird')"> 🦜 پرنده </button> <button onclick="showAnimal('rodent')"> 🐹 جونده </button> </div> <div id="result" class="card"> <h2> به Chiko خوش آمدید </h2> <p> برای دیدن اطلاعات حیوانات، یکی از گزینه‌ها را انتخاب کنید. </p> </div> <footer> © Chiko Animal Care </footer><script> const animals = { dog:{ name:"🐶 سگ", content:` <h2>🐶 سگ</h2> <div class="good"> <h3>✓ خوبی‌ها</h3> <ul> <li>وفادار و بسیار اجتماعی</li> <li>همراه خوب برای خانواده</li> <li>قابل آموزش و باهوش</li> <li>می‌تواند نگهبان خوبی باشد</li> </ul> </div> <div class="bad"> <h3>✕ بدی‌ها</h3> <ul> <li>نیاز به پیاده‌روی روزانه دارد</li> <li>هزینه غذا و دامپزشک ممکن است زیاد باشد</li> <li>نیاز به آموزش و توجه دارد</li> </ul> </div> ` }, cat:{ name:"🐱 گربه", content:` <h2>🐱 گربه</h2> <div class="good"> <h3>✓ خوبی‌ها</h3> <ul> <li>تمیز و مستقل است</li> <li>برای خانه‌های کوچک مناسب است</li> <li>آرام و دوست‌داشتنی است</li> <li>نیاز به مراقبت کمتر از بعضی حیوانات دارد</li> </ul> </div> <div class="bad"> <h3>✕ بدی‌ها</h3> <ul> <li>ممکن است وسایل را چنگ بزند</li> <li>ریزش مو دارد</li> <li>گاهی رفتار مستقل و غیرقابل پیش‌بینی دارد</li> </ul> </div> ` }, bird:{ name:"🦜 پرنده", content:` <h2>🦜 پرنده</h2> <div class="good"> <h3>✓ خوبی‌ها</h3> <ul> <li>زیبا و سرگرم‌کننده است</li> <li>فضای کمی نیاز دارد</li> <li>بعضی پرندگان توانایی یادگیری صدا دارند</li> <li>ارتباط عاطفی خوبی ایجاد می‌کند</li> </ul> </div> <div class="bad"> <h3>✕ بدی‌ها</h3> <ul> <li>به دود، بو و تغییرات محیط حساس است</li> <li>قفس و نظافت منظم لازم دارد</li> <li>تنهایی طولانی برایش خوب نیست</li> </ul> </div> ` }, rodent:{ name:"🐹 جونده", content:` <h2>🐹 جونده</h2> <div class="good"> <h3>✓ خوبی‌ها</h3> <ul> <li>کوچک و بامزه است</li> <li>برای فضای کم مناسب است</li> <li>هزینه نگهداری معمولاً کمتر است</li> <li>تماشای رفتارهایش جذاب است</li> </ul> </div> <div class="bad"> <h3>✕ بدی‌ها</h3> <ul> <li>عمر بعضی گونه‌ها کوتاه است</li> <li>قفس نیاز به تمیزکاری دارد</li> <li>برخی شب‌ها فعال‌تر هستند</li> </ul> </div> ` } }; </script> <script> function showAnimal(animal){ let box = document.getElementById("result"); if(animals[animal]){ box.innerHTML = animals[animal].content; } } // جستجوی حیوانات document.getElementById("searchBox").addEventListener("keyup",function(){ let text = this.value.trim(); text = text.toLowerCase(); if(text.includes("سگ") || text.includes("dog")){ showAnimal("dog"); } else if(text.includes("گربه") || text.includes("cat")){ showAnimal("cat"); } else if(text.includes("پرنده") || text.includes("bird")){ showAnimal("bird"); } else if(text.includes("جونده") || text.includes("همستر") || text.includes("rodent")){ showAnimal("rodent"); } else if(text==""){ document.getElementById("result").innerHTML= ` <h2>🐾 Chiko</h2> <p> نام حیوان را جستجو کنید یا یکی از دکمه‌ها را بزنید. </p> `; } }); </script> </body> </html>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Chiko | Animal Care</title>

<style>

*{
box-sizing:border-box;
font-family:Tahoma, sans-serif;
}

body{
margin:0;
background:#f5eee3;
color:#4b3621;
}

/* هدر */
header{
background:linear-gradient(135deg,#5d4037,#8d6e63);
color:white;
padding:30px;
text-align:center;
border-bottom:5px solid #d7b899;
}

header h1{
font-size:40px;
margin:0;
letter-spacing:2px;
}

header p{
font-size:18px;
}

.creator{
font-size:14px;
color:#f3e5d0;
}


/* جستجو */
.search{
text-align:center;
padding:20px;
}

.search input{
width:85%;
max-width:500px;
padding:15px;
border-radius:30px;
border:2px solid #8d6e63;
font-size:16px;
background:white;
}


/* دکمه ها */
.buttons{
text-align:center;
padding:10px;
}


button{
background:#795548;
color:white;
border:none;
padding:13px 25px;
margin:6px;
border-radius:30px;
font-size:16px;
cursor:pointer;
transition:.3s;
}


button:hover{
background:#4e342e;
transform:scale(1.05);
}


/* کارت اطلاعات */

.card{
background:white;
width:90%;
max-width:700px;
margin:20px auto;
padding:25px;
border-radius:20px;
box-shadow:0 10px 25px #cdb79e;
min-height:200px;
}


.card h2{
color:#6d4c41;
}


.good{
background:#f1e8dc;
padding:10px;
border-radius:10px;
}


.bad{
background:#fff3f0;
padding:10px;
border-radius:10px;
}


footer{
text-align:center;
background:#5d4037;
color:white;
padding:15px;
margin-top:30px;
}


</style>

</head>


<body>


<header>

<h1>🐾 Chiko</h1>

<p>
راهنمای لوکس و کلاسیک نگهداری از حیوانات
</p>

<div class="creator">
طراحی و ساخت: Chiko
</div>

</header>


<div class="search">

<input 
id="searchBox"
placeholder="جستجو حیوان... مثل سگ، گربه، پرنده"
>

</div>


<div class="buttons">

<button onclick="showAnimal('dog')">
🐶 سگ
</button>


<button onclick="showAnimal('cat')">
🐱 گربه
</button>


<button onclick="showAnimal('bird')">
🦜 پرنده
</button>


<button onclick="showAnimal('rodent')">
🐹 جونده
</button>


</div>



<div id="result" class="card">

<h2>
به Chiko خوش آمدید
</h2>

<p>
برای دیدن اطلاعات حیوانات، یکی از گزینه‌ها را انتخاب کنید.
</p>

</div>


<footer>
© Chiko Animal Care
</footer><script>

const animals = {

dog:{

name:"🐶 سگ",

content:`

<h2>🐶 سگ</h2>

<div class="good">

<h3>✓ خوبی‌ها</h3>

<ul>
<li>وفادار و بسیار اجتماعی</li>
<li>همراه خوب برای خانواده</li>
<li>قابل آموزش و باهوش</li>
<li>می‌تواند نگهبان خوبی باشد</li>
</ul>

</div>


<div class="bad">

<h3>✕ بدی‌ها</h3>

<ul>
<li>نیاز به پیاده‌روی روزانه دارد</li>
<li>هزینه غذا و دامپزشک ممکن است زیاد باشد</li>
<li>نیاز به آموزش و توجه دارد</li>
</ul>

</div>

`

},


cat:{

name:"🐱 گربه",

content:`

<h2>🐱 گربه</h2>


<div class="good">

<h3>✓ خوبی‌ها</h3>

<ul>
<li>تمیز و مستقل است</li>
<li>برای خانه‌های کوچک مناسب است</li>
<li>آرام و دوست‌داشتنی است</li>
<li>نیاز به مراقبت کمتر از بعضی حیوانات دارد</li>
</ul>

</div>


<div class="bad">

<h3>✕ بدی‌ها</h3>

<ul>
<li>ممکن است وسایل را چنگ بزند</li>
<li>ریزش مو دارد</li>
<li>گاهی رفتار مستقل و غیرقابل پیش‌بینی دارد</li>
</ul>

</div>

`

},



bird:{

name:"🦜 پرنده",

content:`

<h2>🦜 پرنده</h2>


<div class="good">

<h3>✓ خوبی‌ها</h3>

<ul>
<li>زیبا و سرگرم‌کننده است</li>
<li>فضای کمی نیاز دارد</li>
<li>بعضی پرندگان توانایی یادگیری صدا دارند</li>
<li>ارتباط عاطفی خوبی ایجاد می‌کند</li>
</ul>

</div>


<div class="bad">

<h3>✕ بدی‌ها</h3>

<ul>
<li>به دود، بو و تغییرات محیط حساس است</li>
<li>قفس و نظافت منظم لازم دارد</li>
<li>تنهایی طولانی برایش خوب نیست</li>
</ul>

</div>

`

},



rodent:{

name:"🐹 جونده",

content:`

<h2>🐹 جونده</h2>


<div class="good">

<h3>✓ خوبی‌ها</h3>

<ul>
<li>کوچک و بامزه است</li>
<li>برای فضای کم مناسب است</li>
<li>هزینه نگهداری معمولاً کمتر است</li>
<li>تماشای رفتارهایش جذاب است</li>
</ul>

</div>


<div class="bad">

<h3>✕ بدی‌ها</h3>

<ul>
<li>عمر بعضی گونه‌ها کوتاه است</li>
<li>قفس نیاز به تمیزکاری دارد</li>
<li>برخی شب‌ها فعال‌تر هستند</li>
</ul>

</div>

`

}

};


</script>
<script>

function showAnimal(animal){

let box = document.getElementById("result");

if(animals[animal]){

box.innerHTML = animals[animal].content;

}

}


// جستجوی حیوانات

document.getElementById("searchBox").addEventListener("keyup",function(){

let text = this.value.trim();

text = text.toLowerCase();


if(text.includes("سگ") || text.includes("dog")){

showAnimal("dog");

}

else if(text.includes("گربه") || text.includes("cat")){

showAnimal("cat");

}

else if(text.includes("پرنده") || text.includes("bird")){

showAnimal("bird");

}

else if(text.includes("جونده") || text.includes("همستر") || text.includes("rodent")){

showAnimal("rodent");

}

else if(text==""){

document.getElementById("result").innerHTML=
`
<h2>🐾 Chiko</h2>

<p>
نام حیوان را جستجو کنید یا یکی از دکمه‌ها را بزنید.
</p>
`;

}

});


</script>


</body>
</html>
کیپ
TEXT - 2026-07-06 09:49:14
cd C:\src\test_apk_001 flutter pub remove url_launcher flutter pub remove webview_flutter $pubspecPath = ".\pubspec.yaml" $pubspec = Get-Content $pubspecPath -Raw $pubspec = $pubspec -replace "(?m)^\s*url_launcher:.*\r?\n", "" $pubspec = $pubspec -replace "(?m)^\s*webview_flutter:.*\r?\n", "" if ($pubspec -match "(?m)^dependencies:\s*$") { $pubspec = $pubspec -replace "(?m)^dependencies:\s*$", "dependencies:`r`n webview_flutter: 4.8.0" } Set-Content -Path $pubspecPath -Value $pubspec -Encoding UTF8 $dart = @' import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() { runApp(const FaryazanApp()); } class FaryazanApp extends StatelessWidget { const FaryazanApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'فریازان دکور', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({super.key}); void openWebsiteInsideApp(BuildContext context) { Navigator.push( context, MaterialPageRoute( builder: (context) => const WebsitePage(), ), ); } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, child: Scaffold( appBar: AppBar( title: const Text('فریازان دکور'), centerTitle: true, backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, ), body: Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.chair_alt, size: 90, color: Colors.deepPurple, ), const SizedBox(height: 24), const Text( 'به فریازان دکور خوش آمدید', textAlign: TextAlign.center, style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), const Text( 'فروشگاه آنلاین مبلمان و دکوراسیون', textAlign: TextAlign.center, style: TextStyle(fontSize: 18), ), const SizedBox(height: 12), const Text( 'faryazandecor.com', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, color: Colors.deepPurple, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 32), ElevatedButton.icon( onPressed: () => openWebsiteInsideApp(context), icon: const Icon(Icons.web), label: const Text('باز کردن سایت فریازان دکور'), style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), textStyle: const TextStyle(fontSize: 17), ), ), ], ), ), ), ), ); } } class WebsitePage extends StatefulWidget { const WebsitePage({super.key}); @override State<WebsitePage> createState() => _WebsitePageState(); } class _WebsitePageState extends State<WebsitePage> { late final WebViewController controller; int loadingProgress = 0; @override void initState() { super.initState(); controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { setState(() { loadingProgress = progress; }); }, ), ) ..loadRequest(Uri.parse('https://faryazandecor.com')); } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, child: Scaffold( appBar: AppBar( title: const Text('سایت فریازان دکور'), centerTitle: true, backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, actions: [ IconButton( onPressed: () { controller.reload(); }, icon: const Icon(Icons.refresh), ), ], ), body: Column( children: [ if (loadingProgress < 100) LinearProgressIndicator(value: loadingProgress / 100), Expanded( child: WebViewWidget(controller: controller), ), ], ), ), ); } } '@ Set-Content -Path .\lib\main.dart -Value $dart -Encoding UTF8 $manifestPath = ".\android\app\src\main\AndroidManifest.xml" $manifest = Get-Content $manifestPath -Raw if ($manifest -notmatch "android.permission.INTERNET") { $manifest = $manifest -replace '(<manifest[^>]*>)', ('$1' + "`r`n <uses-permission android:name=""android.permission.INTERNET"" />") Set-Content -Path $manifestPath -Value $manifest -Encoding UTF8 } $gradleKts = ".\android\app\build.gradle.kts" $gradle = ".\android\app\build.gradle" if (Test-Path $gradleKts) { $g = Get-Content $gradleKts -Raw $g = $g -replace "minSdk\s*=\s*flutter\.minSdkVersion", "minSdk = 24" $g = $g -replace "minSdk\s*=\s*\d+", "minSdk = 24" Set-Content -Path $gradleKts -Value $g -Encoding UTF8 } if (Test-Path $gradle) { $g = Get-Content $gradle -Raw $g = $g -replace "minSdkVersion\s+flutter\.minSdkVersion", "minSdkVersion 24" $g = $g -replace "minSdkVersion\s+\d+", "minSdkVersion 24" Set-Content -Path $gradle -Value $g -Encoding UTF8 } Remove-Item -Recurse -Force .\.dart_tool -ErrorAction SilentlyContinue Remove-Item -Recurse -Force .\build -ErrorAction SilentlyContinue Remove-Item -Force .\pubspec.lock -ErrorAction SilentlyContinue flutter clean flutter pub get flutter build apk --release
cd C:\src\test_apk_001

flutter pub remove url_launcher
flutter pub remove webview_flutter

$pubspecPath = ".\pubspec.yaml"
$pubspec = Get-Content $pubspecPath -Raw

$pubspec = $pubspec -replace "(?m)^\s*url_launcher:.*\r?\n", ""
$pubspec = $pubspec -replace "(?m)^\s*webview_flutter:.*\r?\n", ""

if ($pubspec -match "(?m)^dependencies:\s*$") {
  $pubspec = $pubspec -replace "(?m)^dependencies:\s*$", "dependencies:`r`n  webview_flutter: 4.8.0"
}

Set-Content -Path $pubspecPath -Value $pubspec -Encoding UTF8

$dart = @'
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(const FaryazanApp());
}

class FaryazanApp extends StatelessWidget {
  const FaryazanApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'فریازان دکور',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  void openWebsiteInsideApp(BuildContext context) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => const WebsitePage(),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.rtl,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('فریازان دکور'),
          centerTitle: true,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
        ),
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(
                  Icons.chair_alt,
                  size: 90,
                  color: Colors.deepPurple,
                ),
                const SizedBox(height: 24),
                const Text(
                  'به فریازان دکور خوش آمدید',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 16),
                const Text(
                  'فروشگاه آنلاین مبلمان و دکوراسیون',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 18),
                ),
                const SizedBox(height: 12),
                const Text(
                  'faryazandecor.com',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 18,
                    color: Colors.deepPurple,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const SizedBox(height: 32),
                ElevatedButton.icon(
                  onPressed: () => openWebsiteInsideApp(context),
                  icon: const Icon(Icons.web),
                  label: const Text('باز کردن سایت فریازان دکور'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.deepPurple,
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
                    textStyle: const TextStyle(fontSize: 17),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class WebsitePage extends StatefulWidget {
  const WebsitePage({super.key});

  @override
  State<WebsitePage> createState() => _WebsitePageState();
}

class _WebsitePageState extends State<WebsitePage> {
  late final WebViewController controller;
  int loadingProgress = 0;

  @override
  void initState() {
    super.initState();

    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            setState(() {
              loadingProgress = progress;
            });
          },
        ),
      )
      ..loadRequest(Uri.parse('https://faryazandecor.com'));
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.rtl,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('سایت فریازان دکور'),
          centerTitle: true,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
          actions: [
            IconButton(
              onPressed: () {
                controller.reload();
              },
              icon: const Icon(Icons.refresh),
            ),
          ],
        ),
        body: Column(
          children: [
            if (loadingProgress < 100)
              LinearProgressIndicator(value: loadingProgress / 100),
            Expanded(
              child: WebViewWidget(controller: controller),
            ),
          ],
        ),
      ),
    );
  }
}
'@

Set-Content -Path .\lib\main.dart -Value $dart -Encoding UTF8

$manifestPath = ".\android\app\src\main\AndroidManifest.xml"
$manifest = Get-Content $manifestPath -Raw

if ($manifest -notmatch "android.permission.INTERNET") {
  $manifest = $manifest -replace '(<manifest[^>]*>)', ('$1' + "`r`n    <uses-permission android:name=""android.permission.INTERNET"" />")
  Set-Content -Path $manifestPath -Value $manifest -Encoding UTF8
}

$gradleKts = ".\android\app\build.gradle.kts"
$gradle = ".\android\app\build.gradle"

if (Test-Path $gradleKts) {
  $g = Get-Content $gradleKts -Raw
  $g = $g -replace "minSdk\s*=\s*flutter\.minSdkVersion", "minSdk = 24"
  $g = $g -replace "minSdk\s*=\s*\d+", "minSdk = 24"
  Set-Content -Path $gradleKts -Value $g -Encoding UTF8
}

if (Test-Path $gradle) {
  $g = Get-Content $gradle -Raw
  $g = $g -replace "minSdkVersion\s+flutter\.minSdkVersion", "minSdkVersion 24"
  $g = $g -replace "minSdkVersion\s+\d+", "minSdkVersion 24"
  Set-Content -Path $gradle -Value $g -Encoding UTF8
}

Remove-Item -Recurse -Force .\.dart_tool -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force .\build -ErrorAction SilentlyContinue
Remove-Item -Force .\pubspec.lock -ErrorAction SilentlyContinue

flutter clean
flutter pub get
flutter build apk --release
App
TEXT - 2026-07-06 09:30:48
cd C:\src\test_apk_001 flutter pub remove url_launcher flutter pub add webview_flutter $dart = @' import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() { runApp(const FaryazanApp()); } class FaryazanApp extends StatelessWidget { const FaryazanApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'فریازان دکور', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({super.key}); void openWebsiteInsideApp(BuildContext context) { Navigator.push( context, MaterialPageRoute( builder: (context) => const WebsitePage(), ), ); } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, child: Scaffold( appBar: AppBar( title: const Text('فریازان دکور'), centerTitle: true, backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, ), body: Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.chair_alt, size: 90, color: Colors.deepPurple, ), const SizedBox(height: 24), const Text( 'به فریازان دکور خوش آمدید', textAlign: TextAlign.center, style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), const Text( 'فروشگاه آنلاین مبلمان و دکوراسیون', textAlign: TextAlign.center, style: TextStyle(fontSize: 18), ), const SizedBox(height: 12), const Text( 'faryazandecor.com', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, color: Colors.deepPurple, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 32), ElevatedButton.icon( onPressed: () => openWebsiteInsideApp(context), icon: const Icon(Icons.web), label: const Text('باز کردن سایت فریازان دکور'), style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 14, ), textStyle: const TextStyle(fontSize: 17), ), ), ], ), ), ), ), ); } } class WebsitePage extends StatefulWidget { const WebsitePage({super.key}); @override State<WebsitePage> createState() => _WebsitePageState(); } class _WebsitePageState extends State<WebsitePage> { late final WebViewController controller; int loadingProgress = 0; @override void initState() { super.initState(); controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { setState(() { loadingProgress = progress; }); }, ), ) ..loadRequest(Uri.parse('https://faryazandecor.com')); } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, child: Scaffold( appBar: AppBar( title: const Text('سایت فریازان دکور'), centerTitle: true, backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, actions: [ IconButton( onPressed: () { controller.reload(); }, icon: const Icon(Icons.refresh), ), ], ), body: Column( children: [ if (loadingProgress < 100) LinearProgressIndicator( value: loadingProgress / 100, ), Expanded( child: WebViewWidget( controller: controller, ), ), ], ), ), ); } } '@ Set-Content -Path .\lib\main.dart -Value $dart -Encoding UTF8 $manifestPath = ".\android\app\src\main\AndroidManifest.xml" $manifest = Get-Content $manifestPath -Raw if ($manifest -notmatch "android.permission.INTERNET") { $manifest = $manifest -replace '(<manifest[^>]*>)', ('$1' + "`r`n <uses-permission android:name=""android.permission.INTERNET"" />") Set-Content -Path $manifestPath -Value $manifest -Encoding UTF8 } $gradleKts = ".\android\app\build.gradle.kts" $gradle = ".\android\app\build.gradle" if (Test-Path $gradleKts) { $g = Get-Content $gradleKts -Raw $g = $g -replace "minSdk\s*=\s*flutter\.minSdkVersion", "minSdk = 24" $g = $g -replace "minSdk\s*=\s*\d+", "minSdk = 24" Set-Content -Path $gradleKts -Value $g -Encoding UTF8 } if (Test-Path $gradle) { $g = Get-Content $gradle -Raw $g = $g -replace "minSdkVersion\s+flutter\.minSdkVersion", "minSdkVersion 24" $g = $g -replace "minSdkVersion\s+\d+", "minSdkVersion 24" Set-Content -Path $gradle -Value $g -Encoding UTF8 } flutter clean flutter pub get flutter build apk --release
cd C:\src\test_apk_001

flutter pub remove url_launcher
flutter pub add webview_flutter

$dart = @'
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(const FaryazanApp());
}

class FaryazanApp extends StatelessWidget {
  const FaryazanApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'فریازان دکور',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  void openWebsiteInsideApp(BuildContext context) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => const WebsitePage(),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.rtl,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('فریازان دکور'),
          centerTitle: true,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
        ),
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(
                  Icons.chair_alt,
                  size: 90,
                  color: Colors.deepPurple,
                ),
                const SizedBox(height: 24),
                const Text(
                  'به فریازان دکور خوش آمدید',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 28,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 16),
                const Text(
                  'فروشگاه آنلاین مبلمان و دکوراسیون',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 18),
                ),
                const SizedBox(height: 12),
                const Text(
                  'faryazandecor.com',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 18,
                    color: Colors.deepPurple,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const SizedBox(height: 32),
                ElevatedButton.icon(
                  onPressed: () => openWebsiteInsideApp(context),
                  icon: const Icon(Icons.web),
                  label: const Text('باز کردن سایت فریازان دکور'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.deepPurple,
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(
                      horizontal: 24,
                      vertical: 14,
                    ),
                    textStyle: const TextStyle(fontSize: 17),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class WebsitePage extends StatefulWidget {
  const WebsitePage({super.key});

  @override
  State<WebsitePage> createState() => _WebsitePageState();
}

class _WebsitePageState extends State<WebsitePage> {
  late final WebViewController controller;
  int loadingProgress = 0;

  @override
  void initState() {
    super.initState();

    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            setState(() {
              loadingProgress = progress;
            });
          },
        ),
      )
      ..loadRequest(Uri.parse('https://faryazandecor.com'));
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.rtl,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('سایت فریازان دکور'),
          centerTitle: true,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
          actions: [
            IconButton(
              onPressed: () {
                controller.reload();
              },
              icon: const Icon(Icons.refresh),
            ),
          ],
        ),
        body: Column(
          children: [
            if (loadingProgress < 100)
              LinearProgressIndicator(
                value: loadingProgress / 100,
              ),
            Expanded(
              child: WebViewWidget(
                controller: controller,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
'@

Set-Content -Path .\lib\main.dart -Value $dart -Encoding UTF8

$manifestPath = ".\android\app\src\main\AndroidManifest.xml"
$manifest = Get-Content $manifestPath -Raw

if ($manifest -notmatch "android.permission.INTERNET") {
  $manifest = $manifest -replace '(<manifest[^>]*>)', ('$1' + "`r`n    <uses-permission android:name=""android.permission.INTERNET"" />")
  Set-Content -Path $manifestPath -Value $manifest -Encoding UTF8
}

$gradleKts = ".\android\app\build.gradle.kts"
$gradle = ".\android\app\build.gradle"

if (Test-Path $gradleKts) {
  $g = Get-Content $gradleKts -Raw
  $g = $g -replace "minSdk\s*=\s*flutter\.minSdkVersion", "minSdk = 24"
  $g = $g -replace "minSdk\s*=\s*\d+", "minSdk = 24"
  Set-Content -Path $gradleKts -Value $g -Encoding UTF8
}

if (Test-Path $gradle) {
  $g = Get-Content $gradle -Raw
  $g = $g -replace "minSdkVersion\s+flutter\.minSdkVersion", "minSdkVersion 24"
  $g = $g -replace "minSdkVersion\s+\d+", "minSdkVersion 24"
  Set-Content -Path $gradle -Value $g -Encoding UTF8
}

flutter clean
flutter pub get
flutter build apk --release
خخ
TEXT - 2026-07-05 00:01:18
// ==UserScript== // @name Faryazan IKEA TR 3D Downloader // @namespace https://faryazandecor.com/ // @version 101 // @description دکمه دانلود مدل سه‌بعدی GLB/GLTF/USDZ برای صفحات IKEA ترکیه - فریازان دکور // @match https://ikea.com.tr/* // @match https://www.ikea.com.tr/* // @match https://*.ikea.com.tr/* // @match https://www.ikea.com/* // @match https://*.ikea.com/* // @run-at document-start // @grant GM_download // @connect * // ==/UserScript== (function () { 'use strict'; const found = new Map(); let box, btn, panel, statusEl; function decodeDeep(text) { let x = String(text || ''); for (let i = 0; i < 4; i++) { try { const y = decodeURIComponent(x); if (y === x) break; x = y; } catch (e) { break; } } return x; } function cleanUrl(raw) { if (!raw) return ''; let u = String(raw) .replace(/\\u002F/g, '/') .replace(/\\\//g, '/') .replace(/&/g, '&') .replace(/['"`<>]/g, '') .trim(); u = decodeDeep(u); try { if (!/^https?:\/\//i.test(u)) { u = new URL(u, location.href).href; } } catch (e) { return ''; } return u; } function isModelUrl(url) { return /\.(glb|gltf|usdz)(\?|#|$)/i.test(url || ''); } function addCandidate(raw, source) { const url = cleanUrl(raw); if (!url || !isModelUrl(url)) return false; if (!found.has(url)) { found.set(url, { url, source: source || 'unknown' }); updateStatus(); } return true; } function scanText(text, source) { if (!text) return; const variants = [String(text), decodeDeep(String(text))]; for (const t of variants) { const re = /(https?:\/\/[^\s"'<>\\)]+?\.(?:glb|gltf|usdz)(?:\?[^\s"'<>\\)]*)?)/ig; let m; while ((m = re.exec(t)) !== null) { addCandidate(m[1], source); } const reRelative = /(["'(:\s])([^"'()<>\s]+?\.(?:glb|gltf|usdz)(?:\?[^"'()<>\s]*)?)/ig; while ((m = reRelative.exec(t)) !== null) { addCandidate(m[2], source); } } } function scanDocument(doc) { try { const attrs = ['src', 'href', 'data-src', 'data-url', 'data-model', 'poster']; doc.querySelectorAll('*').forEach(el => { attrs.forEach(a => { const v = el.getAttribute && el.getAttribute(a); if (v) scanText(v, 'dom-attr'); }); }); doc.querySelectorAll('script').forEach(s => { if (s.src) scanText(s.src, 'script-src'); if (s.textContent) scanText(s.textContent, 'script-text'); }); scanText(doc.documentElement.innerHTML, 'page-html'); } catch (e) {} } function scanPerformance(win) { try { const entries = win.performance.getEntriesByType('resource') || []; entries.forEach(e => { if (e && e.name) { scanText(e.name, 'network'); } }); } catch (e) {} } function scanFrames(win) { try { scanDocument(win.document); scanPerformance(win); const frames = win.document.querySelectorAll('iframe'); frames.forEach(frame => { try { if (frame.contentWindow && frame.contentDocument) { scanFrames(frame.contentWindow); } } catch (e) {} }); } catch (e) {} } function scanAll() { scanFrames(window); updateStatus(); } function fileNameFromUrl(url) { let ext = 'glb'; const m = url.match(/\.(glb|gltf|usdz)(\?|#|$)/i); if (m) ext = m[1].toLowerCase(); let title = document.title || 'ikea-3d-model'; title = title .replace(/\s+/g, '-') .replace(/[\\/:*?"<>|]/g, '') .replace(/-IKEA.*$/i, '') .slice(0, 70); if (!title) title = 'ikea-3d-model'; return title + '.' + ext; } function downloadModel(url) { setStatus('در حال شروع دانلود...'); const name = fileNameFromUrl(url); try { GM_download({ url: url, name: name, saveAs: true, onload: function () { setStatus('دانلود انجام شد یا در حال ذخیره است.'); }, onerror: function () { setStatus('دانلود مستقیم نشد؛ لینک در تب جدید باز شد. با Ctrl + S ذخیره کن.'); window.open(url, '_blank'); } }); } catch (e) { setStatus('دانلود مستقیم نشد؛ لینک در تب جدید باز شد. با Ctrl + S ذخیره کن.'); window.open(url, '_blank'); } } function setStatus(text) { if (statusEl) statusEl.textContent = text; } function updateStatus() { if (!btn) return; const count = found.size; if (count > 0) { btn.textContent = 'دانلود مدل 3D (' + count + ')'; btn.style.background = '#16a34a'; } else { btn.textContent = 'پیدا کردن مدل 3D'; btn.style.background = '#2563eb'; } } function renderPanel() { if (!panel) return; scanAll(); const urls = Array.from(found.values()); if (!urls.length) { panel.innerHTML = ` <div style="font-weight:700;margin-bottom:8px;">هنوز فایل مدل پیدا نشد</div> <div style="font-size:13px;line-height:1.8;"> 1) اول دکمه نمایش سه‌بعدی خود سایت IKEA را بزن.<br> 2) چند ثانیه صبر کن تا مدل کامل باز شود.<br> 3) دوباره روی این دکمه آبی بزن. </div> `; return; } let html = ` <div style="font-weight:700;margin-bottom:8px;">فایل‌های پیدا شده:</div> `; urls.forEach((item, index) => { const ext = (item.url.match(/\.(glb|gltf|usdz)/i) || ['', 'file'])[1].toUpperCase(); html += ` <button data-url="${item.url.replace(/"/g, '"')}" style=" width:100%; margin:5px 0; padding:9px; border:0; border-radius:8px; background:#16a34a; color:white; cursor:pointer; font-weight:700; "> دانلود ${ext} شماره ${index + 1} </button> `; }); panel.innerHTML = html; panel.querySelectorAll('button[data-url]').forEach(b => { b.addEventListener('click', function () { downloadModel(this.getAttribute('data-url')); }); }); } function createUI() { if (document.getElementById('fz-ikea-3d-downloader')) return; box = document.createElement('div'); box.id = 'fz-ikea-3d-downloader'; box.style.cssText = ` position:fixed; left:18px; bottom:18px; z-index:999999999; font-family:tahoma,arial,sans-serif; direction:rtl; text-align:right; `; btn = document.createElement('button'); btn.textContent = 'پیدا کردن مدل 3D'; btn.style.cssText = ` padding:12px 16px; border:0; border-radius:12px; background:#2563eb; color:white; font-size:14px; font-weight:700; cursor:pointer; box-shadow:0 8px 22px rgba(0,0,0,.25); `; panel = document.createElement('div'); panel.style.cssText = ` display:none; width:310px; max-height:360px; overflow:auto; margin-bottom:10px; padding:12px; background:white; color:#111; border:1px solid #ddd; border-radius:12px; box-shadow:0 8px 25px rgba(0,0,0,.25); font-size:13px; line-height:1.7; `; statusEl = document.createElement('div'); statusEl.style.cssText = ` margin-top:8px; color:#555; font-size:12px; `; statusEl.textContent = 'آماده'; panel.appendChild(statusEl); btn.addEventListener('click', function () { scanAll(); panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; renderPanel(); }); box.appendChild(panel); box.appendChild(btn); document.body.appendChild(box); updateStatus(); } try { const observer = new PerformanceObserver(list => { list.getEntries().forEach(entry => { if (entry && entry.name) { scanText(entry.name, 'live-network'); } }); }); observer.observe({ entryTypes: ['resource'] }); } catch (e) {} setInterval(scanAll, 2500); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createUI); } else { createUI(); } })();
// ==UserScript==
// @name         Faryazan IKEA TR 3D Downloader
// @namespace    https://faryazandecor.com/
// @version      101
// @description  دکمه دانلود مدل سه‌بعدی GLB/GLTF/USDZ برای صفحات IKEA ترکیه - فریازان دکور
// @match        https://ikea.com.tr/*
// @match        https://www.ikea.com.tr/*
// @match        https://*.ikea.com.tr/*
// @match        https://www.ikea.com/*
// @match        https://*.ikea.com/*
// @run-at       document-start
// @grant        GM_download
// @connect      *
// ==/UserScript==

(function () {
    'use strict';

    const found = new Map();
    let box, btn, panel, statusEl;

    function decodeDeep(text) {
        let x = String(text || '');
        for (let i = 0; i < 4; i++) {
            try {
                const y = decodeURIComponent(x);
                if (y === x) break;
                x = y;
            } catch (e) {
                break;
            }
        }
        return x;
    }

    function cleanUrl(raw) {
        if (!raw) return '';
        let u = String(raw)
            .replace(/\\u002F/g, '/')
            .replace(/\\\//g, '/')
            .replace(/&/g, '&')
            .replace(/['"`<>]/g, '')
            .trim();

        u = decodeDeep(u);

        try {
            if (!/^https?:\/\//i.test(u)) {
                u = new URL(u, location.href).href;
            }
        } catch (e) {
            return '';
        }

        return u;
    }

    function isModelUrl(url) {
        return /\.(glb|gltf|usdz)(\?|#|$)/i.test(url || '');
    }

    function addCandidate(raw, source) {
        const url = cleanUrl(raw);
        if (!url || !isModelUrl(url)) return false;

        if (!found.has(url)) {
            found.set(url, {
                url,
                source: source || 'unknown'
            });
            updateStatus();
        }

        return true;
    }

    function scanText(text, source) {
        if (!text) return;

        const variants = [String(text), decodeDeep(String(text))];

        for (const t of variants) {
            const re = /(https?:\/\/[^\s"'<>\\)]+?\.(?:glb|gltf|usdz)(?:\?[^\s"'<>\\)]*)?)/ig;
            let m;
            while ((m = re.exec(t)) !== null) {
                addCandidate(m[1], source);
            }

            const reRelative = /(["'(:\s])([^"'()<>\s]+?\.(?:glb|gltf|usdz)(?:\?[^"'()<>\s]*)?)/ig;
            while ((m = reRelative.exec(t)) !== null) {
                addCandidate(m[2], source);
            }
        }
    }

    function scanDocument(doc) {
        try {
            const attrs = ['src', 'href', 'data-src', 'data-url', 'data-model', 'poster'];

            doc.querySelectorAll('*').forEach(el => {
                attrs.forEach(a => {
                    const v = el.getAttribute && el.getAttribute(a);
                    if (v) scanText(v, 'dom-attr');
                });
            });

            doc.querySelectorAll('script').forEach(s => {
                if (s.src) scanText(s.src, 'script-src');
                if (s.textContent) scanText(s.textContent, 'script-text');
            });

            scanText(doc.documentElement.innerHTML, 'page-html');
        } catch (e) {}
    }

    function scanPerformance(win) {
        try {
            const entries = win.performance.getEntriesByType('resource') || [];
            entries.forEach(e => {
                if (e && e.name) {
                    scanText(e.name, 'network');
                }
            });
        } catch (e) {}
    }

    function scanFrames(win) {
        try {
            scanDocument(win.document);
            scanPerformance(win);

            const frames = win.document.querySelectorAll('iframe');
            frames.forEach(frame => {
                try {
                    if (frame.contentWindow && frame.contentDocument) {
                        scanFrames(frame.contentWindow);
                    }
                } catch (e) {}
            });
        } catch (e) {}
    }

    function scanAll() {
        scanFrames(window);
        updateStatus();
    }

    function fileNameFromUrl(url) {
        let ext = 'glb';
        const m = url.match(/\.(glb|gltf|usdz)(\?|#|$)/i);
        if (m) ext = m[1].toLowerCase();

        let title = document.title || 'ikea-3d-model';
        title = title
            .replace(/\s+/g, '-')
            .replace(/[\\/:*?"<>|]/g, '')
            .replace(/-IKEA.*$/i, '')
            .slice(0, 70);

        if (!title) title = 'ikea-3d-model';

        return title + '.' + ext;
    }

    function downloadModel(url) {
        setStatus('در حال شروع دانلود...');

        const name = fileNameFromUrl(url);

        try {
            GM_download({
                url: url,
                name: name,
                saveAs: true,
                onload: function () {
                    setStatus('دانلود انجام شد یا در حال ذخیره است.');
                },
                onerror: function () {
                    setStatus('دانلود مستقیم نشد؛ لینک در تب جدید باز شد. با Ctrl + S ذخیره کن.');
                    window.open(url, '_blank');
                }
            });
        } catch (e) {
            setStatus('دانلود مستقیم نشد؛ لینک در تب جدید باز شد. با Ctrl + S ذخیره کن.');
            window.open(url, '_blank');
        }
    }

    function setStatus(text) {
        if (statusEl) statusEl.textContent = text;
    }

    function updateStatus() {
        if (!btn) return;

        const count = found.size;

        if (count > 0) {
            btn.textContent = 'دانلود مدل 3D (' + count + ')';
            btn.style.background = '#16a34a';
        } else {
            btn.textContent = 'پیدا کردن مدل 3D';
            btn.style.background = '#2563eb';
        }
    }

    function renderPanel() {
        if (!panel) return;

        scanAll();

        const urls = Array.from(found.values());

        if (!urls.length) {
            panel.innerHTML = `
                <div style="font-weight:700;margin-bottom:8px;">هنوز فایل مدل پیدا نشد</div>
                <div style="font-size:13px;line-height:1.8;">
                    1) اول دکمه نمایش سه‌بعدی خود سایت IKEA را بزن.<br>
                    2) چند ثانیه صبر کن تا مدل کامل باز شود.<br>
                    3) دوباره روی این دکمه آبی بزن.
                </div>
            `;
            return;
        }

        let html = `
            <div style="font-weight:700;margin-bottom:8px;">فایل‌های پیدا شده:</div>
        `;

        urls.forEach((item, index) => {
            const ext = (item.url.match(/\.(glb|gltf|usdz)/i) || ['', 'file'])[1].toUpperCase();
            html += `
                <button data-url="${item.url.replace(/"/g, '"')}"
                    style="
                        width:100%;
                        margin:5px 0;
                        padding:9px;
                        border:0;
                        border-radius:8px;
                        background:#16a34a;
                        color:white;
                        cursor:pointer;
                        font-weight:700;
                    ">
                    دانلود ${ext} شماره ${index + 1}
                </button>
            `;
        });

        panel.innerHTML = html;

        panel.querySelectorAll('button[data-url]').forEach(b => {
            b.addEventListener('click', function () {
                downloadModel(this.getAttribute('data-url'));
            });
        });
    }

    function createUI() {
        if (document.getElementById('fz-ikea-3d-downloader')) return;

        box = document.createElement('div');
        box.id = 'fz-ikea-3d-downloader';
        box.style.cssText = `
            position:fixed;
            left:18px;
            bottom:18px;
            z-index:999999999;
            font-family:tahoma,arial,sans-serif;
            direction:rtl;
            text-align:right;
        `;

        btn = document.createElement('button');
        btn.textContent = 'پیدا کردن مدل 3D';
        btn.style.cssText = `
            padding:12px 16px;
            border:0;
            border-radius:12px;
            background:#2563eb;
            color:white;
            font-size:14px;
            font-weight:700;
            cursor:pointer;
            box-shadow:0 8px 22px rgba(0,0,0,.25);
        `;

        panel = document.createElement('div');
        panel.style.cssText = `
            display:none;
            width:310px;
            max-height:360px;
            overflow:auto;
            margin-bottom:10px;
            padding:12px;
            background:white;
            color:#111;
            border:1px solid #ddd;
            border-radius:12px;
            box-shadow:0 8px 25px rgba(0,0,0,.25);
            font-size:13px;
            line-height:1.7;
        `;

        statusEl = document.createElement('div');
        statusEl.style.cssText = `
            margin-top:8px;
            color:#555;
            font-size:12px;
        `;
        statusEl.textContent = 'آماده';

        panel.appendChild(statusEl);

        btn.addEventListener('click', function () {
            scanAll();
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
            renderPanel();
        });

        box.appendChild(panel);
        box.appendChild(btn);
        document.body.appendChild(box);

        updateStatus();
    }

    try {
        const observer = new PerformanceObserver(list => {
            list.getEntries().forEach(entry => {
                if (entry && entry.name) {
                    scanText(entry.name, 'live-network');
                }
            });
        });
        observer.observe({ entryTypes: ['resource'] });
    } catch (e) {}

    setInterval(scanAll, 2500);

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', createUI);
    } else {
        createUI();
    }

})();
Jjj
TEXT - 2026-06-30 09:31:15
cd C:\src\test_apk_001 flutter pub add webview_flutter $dart = @' import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() { runApp(const FaryazanApp()); } class FaryazanApp extends StatelessWidget { const FaryazanApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'فریازان دکور', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({super.key}); void openWebsiteInsideApp(BuildContext context) { Navigator.push( context, MaterialPageRoute( builder: (context) => const WebsitePage(), ), ); } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, child: Scaffold( appBar: AppBar( title: const Text('فریازان دکور'), centerTitle: true, backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, ), body: Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.chair_alt, size: 90, color: Colors.deepPurple, ), const SizedBox(height: 24), const Text( 'به فریازان دکور خوش آمدید', textAlign: TextAlign.center, style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), const Text( 'فروشگاه آنلاین مبلمان و دکوراسیون', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, ), ), const SizedBox(height: 12), const Text( 'faryazandecor.com', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, color: Colors.deepPurple, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 32), ElevatedButton.icon( onPressed: () => openWebsiteInsideApp(context), icon: const Icon(Icons.web), label: const Text('باز کردن سایت فریازان دکور'), style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 14, ), textStyle: const TextStyle(fontSize: 17), ), ), ], ), ), ), ), ); } } class WebsitePage extends StatefulWidget { const WebsitePage({super.key}); @override State<WebsitePage> createState() => _WebsitePageState(); } class _WebsitePageState extends State<WebsitePage> { late final WebViewController controller; int loadingProgress = 0; @override void initState() { super.initState(); controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { setState(() { loadingProgress = progress; }); }, ), ) ..loadRequest(Uri.parse('https://faryazandecor.com')); } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, child: Scaffold( appBar: AppBar( title: const Text('سایت فریازان دکور'), centerTitle: true, backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, actions: [ IconButton( onPressed: () { controller.reload(); }, icon: const Icon(Icons.refresh), ), ], ), body: Column( children: [ if (loadingProgress < 100) LinearProgressIndicator( value: loadingProgress / 100, ), Expanded( child: WebViewWidget( controller: controller, ), ), ], ), ), ); } } '@ Set-Content -Path .\lib\main.dart -Value $dart -Encoding UTF8 $manifestPath = ".\android\app\src\main\AndroidManifest.xml" $manifest = Get-Content $manifestPath -Raw if ($manifest -notmatch "android.permission.INTERNET") { $manifest = $manifest -replace '(<manifest[^>]*>)', ('$1' + "`r`n <uses-permission android:name=""android.permission.INTERNET"" />") Set-Content -Path $manifestPath -Value $manifest -Encoding UTF8 } $gradleKts = ".\android\app\build.gradle.kts" $gradle = ".\android\app\build.gradle" if (Test-Path $gradleKts) { $g = Get-Content $gradleKts -Raw if ($g -match "minSdk\s*=") { $g = $g -replace "minSdk\s*=\s*flutter\.minSdkVersion", "minSdk = 24" $g = $g -replace "minSdk\s*=\s*\d+", "minSdk = 24" } else { $g = $g -replace "defaultConfig\s*\{", "defaultConfig {`r`n minSdk = 24" } Set-Content -Path $gradleKts -Value $g -Encoding UTF8 } if (Test-Path $gradle) { $g = Get-Content $gradle -Raw if ($g -match "minSdkVersion") { $g = $g -replace "minSdkVersion\s+flutter\.minSdkVersion", "minSdkVersion 24" $g = $g -replace "minSdkVersion\s+\d+", "minSdkVersion 24" } else { $g = $g -replace "defaultConfig\s*\{", "defaultConfig {`r`n minSdkVersion 24" } Set-Content -Path $gradle -Value $g -Encoding UTF8 } flutter pub get flutter build apk --release
cd C:\src\test_apk_001

flutter pub add webview_flutter

$dart = @'
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(const FaryazanApp());
}

class FaryazanApp extends StatelessWidget {
  const FaryazanApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'فریازان دکور',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  void openWebsiteInsideApp(BuildContext context) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => const WebsitePage(),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.rtl,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('فریازان دکور'),
          centerTitle: true,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
        ),
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(
                  Icons.chair_alt,
                  size: 90,
                  color: Colors.deepPurple,
                ),
                const SizedBox(height: 24),
                const Text(
                  'به فریازان دکور خوش آمدید',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 28,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 16),
                const Text(
                  'فروشگاه آنلاین مبلمان و دکوراسیون',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 18,
                  ),
                ),
                const SizedBox(height: 12),
                const Text(
                  'faryazandecor.com',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 18,
                    color: Colors.deepPurple,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const SizedBox(height: 32),
                ElevatedButton.icon(
                  onPressed: () => openWebsiteInsideApp(context),
                  icon: const Icon(Icons.web),
                  label: const Text('باز کردن سایت فریازان دکور'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.deepPurple,
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(
                      horizontal: 24,
                      vertical: 14,
                    ),
                    textStyle: const TextStyle(fontSize: 17),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class WebsitePage extends StatefulWidget {
  const WebsitePage({super.key});

  @override
  State<WebsitePage> createState() => _WebsitePageState();
}

class _WebsitePageState extends State<WebsitePage> {
  late final WebViewController controller;
  int loadingProgress = 0;

  @override
  void initState() {
    super.initState();

    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            setState(() {
              loadingProgress = progress;
            });
          },
        ),
      )
      ..loadRequest(Uri.parse('https://faryazandecor.com'));
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.rtl,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('سایت فریازان دکور'),
          centerTitle: true,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
          actions: [
            IconButton(
              onPressed: () {
                controller.reload();
              },
              icon: const Icon(Icons.refresh),
            ),
          ],
        ),
        body: Column(
          children: [
            if (loadingProgress < 100)
              LinearProgressIndicator(
                value: loadingProgress / 100,
              ),
            Expanded(
              child: WebViewWidget(
                controller: controller,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
'@

Set-Content -Path .\lib\main.dart -Value $dart -Encoding UTF8

$manifestPath = ".\android\app\src\main\AndroidManifest.xml"
$manifest = Get-Content $manifestPath -Raw

if ($manifest -notmatch "android.permission.INTERNET") {
  $manifest = $manifest -replace '(<manifest[^>]*>)', ('$1' + "`r`n    <uses-permission android:name=""android.permission.INTERNET"" />")
  Set-Content -Path $manifestPath -Value $manifest -Encoding UTF8
}

$gradleKts = ".\android\app\build.gradle.kts"
$gradle = ".\android\app\build.gradle"

if (Test-Path $gradleKts) {
  $g = Get-Content $gradleKts -Raw
  if ($g -match "minSdk\s*=") {
    $g = $g -replace "minSdk\s*=\s*flutter\.minSdkVersion", "minSdk = 24"
    $g = $g -replace "minSdk\s*=\s*\d+", "minSdk = 24"
  } else {
    $g = $g -replace "defaultConfig\s*\{", "defaultConfig {`r`n        minSdk = 24"
  }
  Set-Content -Path $gradleKts -Value $g -Encoding UTF8
}

if (Test-Path $gradle) {
  $g = Get-Content $gradle -Raw
  if ($g -match "minSdkVersion") {
    $g = $g -replace "minSdkVersion\s+flutter\.minSdkVersion", "minSdkVersion 24"
    $g = $g -replace "minSdkVersion\s+\d+", "minSdkVersion 24"
  } else {
    $g = $g -replace "defaultConfig\s*\{", "defaultConfig {`r`n        minSdkVersion 24"
  }
  Set-Content -Path $gradle -Value $g -Encoding UTF8
}

flutter pub get
flutter build apk --release
Androiid
TEXT - 2026-06-29 22:29:30
import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; void main() { runApp(const FaryazanApp()); } class FaryazanApp extends StatelessWidget { const FaryazanApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'فریازان دکور', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({super.key}); Future<void> openWebsite() async { final Uri url = Uri.parse('https://faryazandecor.com'); if (!await launchUrl( url, mode: LaunchMode.externalApplication, )) { throw Exception('سایت باز نشد'); } } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, child: Scaffold( appBar: AppBar( title: const Text('فریازان دکور'), centerTitle: true, backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, ), body: Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.chair_alt, size: 90, color: Colors.deepPurple, ), const SizedBox(height: 24), const Text( 'به فریازان دکور خوش آمدید', textAlign: TextAlign.center, style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), const Text( 'فروشگاه آنلاین مبلمان و دکوراسیون', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, ), ), const SizedBox(height: 12), const Text( 'faryazandecor.com', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, color: Colors.deepPurple, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 32), ElevatedButton.icon( onPressed: openWebsite, icon: const Icon(Icons.open_in_browser), label: const Text('باز کردن سایت فریازان دکور'), style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 14, ), textStyle: const TextStyle(fontSize: 17), ), ), ], ), ), ), ), ); } }
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

void main() {
  runApp(const FaryazanApp());
}

class FaryazanApp extends StatelessWidget {
  const FaryazanApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'فریازان دکور',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  Future<void> openWebsite() async {
    final Uri url = Uri.parse('https://faryazandecor.com');

    if (!await launchUrl(
      url,
      mode: LaunchMode.externalApplication,
    )) {
      throw Exception('سایت باز نشد');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.rtl,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('فریازان دکور'),
          centerTitle: true,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
        ),
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(
                  Icons.chair_alt,
                  size: 90,
                  color: Colors.deepPurple,
                ),
                const SizedBox(height: 24),
                const Text(
                  'به فریازان دکور خوش آمدید',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 28,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 16),
                const Text(
                  'فروشگاه آنلاین مبلمان و دکوراسیون',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 18,
                  ),
                ),
                const SizedBox(height: 12),
                const Text(
                  'faryazandecor.com',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: 18,
                    color: Colors.deepPurple,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const SizedBox(height: 32),
                ElevatedButton.icon(
                  onPressed: openWebsite,
                  icon: const Icon(Icons.open_in_browser),
                  label: const Text('باز کردن سایت فریازان دکور'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.deepPurple,
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(
                      horizontal: 24,
                      vertical: 14,
                    ),
                    textStyle: const TextStyle(fontSize: 17),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
محصولات موجود
TEXT - 2026-06-14 22:10:06
/************* * آماده تحویل – متاباکس + نمایش در محصول و لیست *************/ /*----------------------------- متاباکس در صفحه محصول -----------------------------*/ add_action( 'add_meta_boxes', 'fzd_ready_add_metabox' ); function fzd_ready_add_metabox() { add_meta_box( 'fzd_ready_box', 'آماده تحویل', 'fzd_ready_metabox_callback', 'product', 'side', 'high' ); } function fzd_ready_metabox_callback( $post ) { $rows = get_post_meta( $post->ID, '_fzd_ready_rows', true ); if ( ! is_array( $rows ) || empty( $rows ) ) { // فقط یک ردیف خالی، بدون مقدار پیش‌فرض $rows = array( array( 'color' => '', 'days' => '' ), ); } $note = get_post_meta( $post->ID, '_fzd_ready_note', true ); wp_nonce_field( 'fzd_ready_save', 'fzd_ready_nonce' ); echo '<p>برای هر رنگ آماده تحویل، یک ردیف وارد کن.</p>'; echo '<div id="fzd-ready-rows">'; foreach ( $rows as $row ) { $color = isset( $row['color'] ) ? $row['color'] : ''; $days = isset( $row['days'] ) ? (int) $row['days'] : 3; echo '<div class="fzd-ready-row" style="margin-bottom:6px;border-bottom:1px solid #ddd;padding-bottom:6px;">'; echo '<input type="text" name="fzd_ready_color[]" value="' . esc_attr( $color ) . '" placeholder="رنگ (مثلاً خودرنگ)" style="width:100%;margin-bottom:4px;">'; echo '<input type="number" name="fzd_ready_days[]" value="' . esc_attr( $days ) . '" min="0" max="365" style="width:100%;margin-bottom:4px;" placeholder="روز تحویل">'; echo '<button type="button" class="button fzd-ready-remove">حذف</button>'; echo '</div>'; } echo '</div>'; echo '<button type="button" class="button button-secondary" id="fzd-ready-add">+ افزودن رنگ دیگر</button>'; echo '<hr><p><strong>توضیح اضافه (اختیاری):</strong><br><small>این متن به رنگ سبز، زیر توضیحات آماده تحویل در صفحه محصول و دسته‌بندی نمایش داده می‌شود.</small></p>'; echo '<textarea name="fzd_ready_note" style="width:100%;min-height:70px;">' . esc_textarea( $note ) . '</textarea>'; ?> <script> (function($){ $(function(){ var $wrap = $('#fzd-ready-rows'); $('#fzd-ready-add').on('click', function(e){ e.preventDefault(); var $first = $wrap.find('.fzd-ready-row:first').clone(); $first.find('input').val(''); $wrap.append($first); }); $wrap.on('click', '.fzd-ready-remove', function(e){ e.preventDefault(); if ($wrap.find('.fzd-ready-row').length > 1) { $(this).closest('.fzd-ready-row').remove(); } else { $(this).closest('.fzd-ready-row').find('input').val(''); } }); }); })(jQuery); </script> <?php } add_action( 'save_post_product', 'fzd_ready_save_metabox' ); function fzd_ready_save_metabox( $post_id ) { if ( ! isset( $_POST['fzd_ready_nonce'] ) || ! wp_verify_nonce( $_POST['fzd_ready_nonce'], 'fzd_ready_save' ) ) return; if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return; // رنگ‌ها + روزها if ( isset( $_POST['fzd_ready_color'], $_POST['fzd_ready_days'] ) ) { $colors = (array) $_POST['fzd_ready_color']; $days = (array) $_POST['fzd_ready_days']; $rows = array(); foreach ( $colors as $i => $color ) { $color = sanitize_text_field( wp_unslash( $color ) ); $d = isset( $days[ $i ] ) ? (int) $days[ $i ] : 0; if ( $color === '' ) continue; if ( $d < 1 ) $d = 1; $rows[] = array( 'color' => $color, 'days' => $d, ); } if ( ! empty( $rows ) ) { update_post_meta( $post_id, '_fzd_ready_rows', $rows ); } else { delete_post_meta( $post_id, '_fzd_ready_rows' ); } }// توضیح دستی if ( isset( $_POST['fzd_ready_note'] ) ) { $note = sanitize_textarea_field( wp_unslash( $_POST['fzd_ready_note'] ) ); if ( $note !== '' ) { update_post_meta( $post_id, '_fzd_ready_note', $note ); } else { delete_post_meta( $post_id, '_fzd_ready_note' ); } } } /*----------------------------- توابع کمکی (شمسی + ارقام فارسی) -----------------------------*/ function fzd_ready_get_rows( $product_id ) { $rows = get_post_meta( $product_id, '_fzd_ready_rows', true ); return is_array( $rows ) ? $rows : array(); } function fzd_ready_get_note( $product_id ) { $note = get_post_meta( $product_id, '_fzd_ready_note', true ); return trim( (string) $note ); } function fzd_ready_persian_digits( $str ) { $en = array('0','1','2','3','4','5','6','7','8','9'); $fa = array('۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'); return str_replace( $en, $fa, (string) $str ); } function fzd_ready_convert_jalali( $timestamp ) { $gy = (int) gmdate( 'Y', $timestamp ); $gm = (int) gmdate( 'n', $timestamp ); $gd = (int) gmdate( 'j', $timestamp ); $g_d_m = array(0,31,59,90,120,151,181,212,243,273,304,334); if ( $gy > 1600 ) { $jy = 979; $gy -= 1600; } else { $jy = 0; $gy -= 621; } $gy2 = ( $gm > 2 ) ? ( $gy + 1 ) : $gy; $days = 365*$gy + (int)(($gy2+3)/4) - (int)(($gy2+99)/100) + (int)(($gy2+399)/400) - 80 + $gd + $g_d_m[$gm-1]; $jy += 33*(int)($days/12053); $days %= 12053; $jy += 4*(int)($days/1461); $days %= 1461; if ( $days > 365 ) { $jy += (int)(($days-1)/365); $days = ($days-1)%365; } if ( $days < 186 ) { $jm = 1 + (int)($days/31); $jd = 1 + ($days%31); } else { $jm = 7 + (int)(($days-186)/30); $jd = 1 + (($days-186)%30); } $months = array( 1=>'فروردین',2=>'اردیبهشت',3=>'خرداد',4=>'تیر',5=>'مرداد',6=>'شهریور', 7=>'مهر',8=>'آبان',9=>'آذر',10=>'دی',11=>'بهمن',12=>'اسفند', ); $day_fa = fzd_ready_persian_digits( $jd ); $month_fa = isset( $months[$jm] ) ? $months[$jm] : ''; return $day_fa . ' ' . $month_fa; } /*----------------------------- نمایش داخل صفحه محصول -----------------------------*/ add_action( 'woocommerce_single_product_summary', 'fzd_ready_single_box', 12 ); function fzd_ready_single_box() { if ( ! is_product() ) return; $product_id = get_the_ID(); if ( ! $product_id ) return; $product = wc_get_product( $product_id ); if ( ! $product || ! $product->is_in_stock() ) return; $rows = fzd_ready_get_rows( $product_id ); if ( empty( $rows ) ) return; echo '<div style="margin-top:10px;margin-bottom:10px;padding:8px 12px;border:1px solid #e53935;border-radius:6px;font-size:16px;line-height:1.9;">'; echo '<strong>رنگ‌های آماده تحویل:</strong>'; echo '<ul style="margin:5px 0 0 0;padding-right:18px;list-style:disc;">'; foreach ( $rows as $row ) { $color = isset( $row['color'] ) ? $row['color'] : ''; $days = isset( $row['days'] ) ? (int) $row['days'] : 1; if ( $color === '' ) continue; if ( $days < 1 ) $days = 1; $ts = current_time( 'timestamp' ) + $days * DAY_IN_SECONDS; $date = fzd_ready_convert_jalali( $ts ); $days_fa = fzd_ready_persian_digits( $days ); echo '<li>این محصول را در رنگ <strong>' . esc_html( $color ) . '</strong> تا <strong>' . esc_html( $date ) . '</strong> تحویل بگیرید (حدود ' . $days_fa . ' روزه).</li>'; } echo '</ul>'; // متن پیش‌فرض – مشکی $default_note = 'سایر رنگ‌ها به صورت سفارشی تولید می‌شوند و زمان تحویل آن‌ها کمی بیشتر است؛ پس از ثبت سفارش، زمان دقیق با شما هماهنگ می‌شود.'; echo '<p style="margin-top:8px;font-size:14px;color:#333333;">' . esc_html( $default_note ) . '</p>'; // توضیح دستی – سبز $note = fzd_ready_get_note( $product_id ); if ( $note !== '' ) { echo '<p style="margin-top:2px;font-size:16px;color:#388e3c;">' . esc_html( $note ) . '</p>'; } echo '</div>'; }/*----------------------------- لیبل «آماده تحویل» روی عکس (فلت‌سام) -----------------------------*/ add_action( 'flatsome_woocommerce_shop_loop_images', 'fzd_ready_badge', 20 ); function fzd_ready_badge() { global $product; if ( ! $product || ! is_a( $product, 'WC_Product' ) ) return; if ( ! $product->is_in_stock() ) return; $rows = fzd_ready_get_rows( $product->get_id() ); if ( empty( $rows ) ) return; // استایل فقط یک بار چاپ شود static $printed = false; if ( ! $printed ) { echo '<style> .product-small .box-image { position: relative; } /* لیبل آماده تحویل – گوشه بالا راست */ .product-small .box-image .fzd-ready-badge { position: absolute; top: -2px; right: 8px; z-index: 10; } /* بادج تخفیف فلت‌سام – بیاد گوشه بالا چپ */ .product-small .box-image .badge-container { left: 8px; right: auto; } </style>'; $printed = true; } echo '<span class="fzd-ready-badge" style="display:inline-block;background:#e53935;color:#ffffff;padding:3px 10px;border-radius:16px;font-size:14px;">آماده تحویل</span>'; } /*----------------------------- متن تحویل + توضیح سبز در لیست محصولات -----------------------------*/ add_action( 'woocommerce_after_shop_loop_item_title', 'fzd_ready_loop_text', 15 ); function fzd_ready_loop_text() { global $product; if ( ! $product || ! is_a( $product, 'WC_Product' ) ) return; if ( ! $product->is_in_stock() ) return; $rows = fzd_ready_get_rows( $product->get_id() ); if ( empty( $rows ) ) return; $max = 2; // حداکثر دو رنگ در دسته‌بندی $count = 0; // متن قرمز زیر محصول echo '<div style="margin-top:4px;font-size:14px;color:#c62828;line-height:1.7;">'; foreach ( $rows as $row ) { if ( $count >= $max ) break; $color = isset( $row['color'] ) ? $row['color'] : ''; $days = isset( $row['days'] ) ? (int) $row['days'] : 1; if ( $color === '' ) continue; if ( $days < 1 ) $days = 1; $ts = current_time( 'timestamp' ) + $days * DAY_IN_SECONDS; $date = fzd_ready_convert_jalali( $ts ); echo 'رنگ ' . esc_html( $color ) . ' را تا ' . esc_html( $date ) . ' تحویل بگیرید<br>'; $count++; } echo '</div>'; // توضیح دستی سبز $note = fzd_ready_get_note( $product->get_id() ); if ( $note !== '' ) { echo '<div style="margin-top:2px;font-size:14px;color:#388e3c;line-height:1.6;">' . esc_html( $note ) . '</div>'; } }
/*************
 * آماده تحویل – متاباکس + نمایش در محصول و لیست
 *************/

/*-----------------------------
  متاباکس در صفحه محصول
-----------------------------*/
add_action( 'add_meta_boxes', 'fzd_ready_add_metabox' );
function fzd_ready_add_metabox() {
    add_meta_box(
        'fzd_ready_box',
        'آماده تحویل',
        'fzd_ready_metabox_callback',
        'product',
        'side',
        'high'
    );
}

function fzd_ready_metabox_callback( $post ) {
    $rows = get_post_meta( $post->ID, '_fzd_ready_rows', true );
if ( ! is_array( $rows ) || empty( $rows ) ) {
    // فقط یک ردیف خالی، بدون مقدار پیش‌فرض
    $rows = array(
        array( 'color' => '', 'days' => '' ),
    );
}

    $note = get_post_meta( $post->ID, '_fzd_ready_note', true );

    wp_nonce_field( 'fzd_ready_save', 'fzd_ready_nonce' );

    echo '<p>برای هر رنگ آماده تحویل، یک ردیف وارد کن.</p>';
    echo '<div id="fzd-ready-rows">';

    foreach ( $rows as $row ) {
        $color = isset( $row['color'] ) ? $row['color'] : '';
        $days  = isset( $row['days'] )  ? (int) $row['days'] : 3;

        echo '<div class="fzd-ready-row" style="margin-bottom:6px;border-bottom:1px solid #ddd;padding-bottom:6px;">';
        echo '<input type="text" name="fzd_ready_color[]" value="' . esc_attr( $color ) . '" placeholder="رنگ (مثلاً خودرنگ)" style="width:100%;margin-bottom:4px;">';
        echo '<input type="number" name="fzd_ready_days[]" value="' . esc_attr( $days ) . '" min="0" max="365" style="width:100%;margin-bottom:4px;" placeholder="روز تحویل">';
        echo '<button type="button" class="button fzd-ready-remove">حذف</button>';
        echo '</div>';
    }

    echo '</div>';
    echo '<button type="button" class="button button-secondary" id="fzd-ready-add">+ افزودن رنگ دیگر</button>';

    echo '<hr><p><strong>توضیح اضافه (اختیاری):</strong><br><small>این متن به رنگ سبز، زیر توضیحات آماده تحویل در صفحه محصول و دسته‌بندی نمایش داده می‌شود.</small></p>';
    echo '<textarea name="fzd_ready_note" style="width:100%;min-height:70px;">' . esc_textarea( $note ) . '</textarea>';

    ?>
    <script>
    (function($){
        $(function(){
            var $wrap = $('#fzd-ready-rows');
            $('#fzd-ready-add').on('click', function(e){
                e.preventDefault();
                var $first = $wrap.find('.fzd-ready-row:first').clone();
                $first.find('input').val('');
                $wrap.append($first);
            });
            $wrap.on('click', '.fzd-ready-remove', function(e){
                e.preventDefault();
                if ($wrap.find('.fzd-ready-row').length > 1) {
                    $(this).closest('.fzd-ready-row').remove();
                } else {
                    $(this).closest('.fzd-ready-row').find('input').val('');
                }
            });
        });
    })(jQuery);
    </script>
    <?php
}

add_action( 'save_post_product', 'fzd_ready_save_metabox' );
function fzd_ready_save_metabox( $post_id ) {
    if ( ! isset( $_POST['fzd_ready_nonce'] ) ||
         ! wp_verify_nonce( $_POST['fzd_ready_nonce'], 'fzd_ready_save' ) ) return;
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;

    // رنگ‌ها + روزها
    if ( isset( $_POST['fzd_ready_color'], $_POST['fzd_ready_days'] ) ) {
        $colors = (array) $_POST['fzd_ready_color'];
        $days   = (array) $_POST['fzd_ready_days'];

        $rows = array();
        foreach ( $colors as $i => $color ) {
            $color = sanitize_text_field( wp_unslash( $color ) );
            $d     = isset( $days[ $i ] ) ? (int) $days[ $i ] : 0;

            if ( $color === '' ) continue;
            if ( $d < 1 ) $d = 1;

            $rows[] = array(
                'color' => $color,
                'days'  => $d,
            );
        }

        if ( ! empty( $rows ) ) {
            update_post_meta( $post_id, '_fzd_ready_rows', $rows );
        } else {
            delete_post_meta( $post_id, '_fzd_ready_rows' );
        }
    }// توضیح دستی
    if ( isset( $_POST['fzd_ready_note'] ) ) {
        $note = sanitize_textarea_field( wp_unslash( $_POST['fzd_ready_note'] ) );
        if ( $note !== '' ) {
            update_post_meta( $post_id, '_fzd_ready_note', $note );
        } else {
            delete_post_meta( $post_id, '_fzd_ready_note' );
        }
    }
}

/*-----------------------------
  توابع کمکی (شمسی + ارقام فارسی)
-----------------------------*/
function fzd_ready_get_rows( $product_id ) {
    $rows = get_post_meta( $product_id, '_fzd_ready_rows', true );
    return is_array( $rows ) ? $rows : array();
}

function fzd_ready_get_note( $product_id ) {
    $note = get_post_meta( $product_id, '_fzd_ready_note', true );
    return trim( (string) $note );
}

function fzd_ready_persian_digits( $str ) {
    $en = array('0','1','2','3','4','5','6','7','8','9');
    $fa = array('۰','۱','۲','۳','۴','۵','۶','۷','۸','۹');
    return str_replace( $en, $fa, (string) $str );
}

function fzd_ready_convert_jalali( $timestamp ) {
    $gy = (int) gmdate( 'Y', $timestamp );
    $gm = (int) gmdate( 'n', $timestamp );
    $gd = (int) gmdate( 'j', $timestamp );

    $g_d_m = array(0,31,59,90,120,151,181,212,243,273,304,334);

    if ( $gy > 1600 ) { $jy = 979; $gy -= 1600; }
    else { $jy = 0; $gy -= 621; }

    $gy2 = ( $gm > 2 ) ? ( $gy + 1 ) : $gy;
    $days = 365*$gy + (int)(($gy2+3)/4) - (int)(($gy2+99)/100) + (int)(($gy2+399)/400) - 80 + $gd + $g_d_m[$gm-1];

    $jy += 33*(int)($days/12053); $days %= 12053;
    $jy += 4*(int)($days/1461);   $days %= 1461;

    if ( $days > 365 ) { $jy += (int)(($days-1)/365); $days = ($days-1)%365; }

    if ( $days < 186 ) { $jm = 1 + (int)($days/31); $jd = 1 + ($days%31); }
    else { $jm = 7 + (int)(($days-186)/30); $jd = 1 + (($days-186)%30); }

    $months = array(
        1=>'فروردین',2=>'اردیبهشت',3=>'خرداد',4=>'تیر',5=>'مرداد',6=>'شهریور',
        7=>'مهر',8=>'آبان',9=>'آذر',10=>'دی',11=>'بهمن',12=>'اسفند',
    );

    $day_fa   = fzd_ready_persian_digits( $jd );
    $month_fa = isset( $months[$jm] ) ? $months[$jm] : '';

    return $day_fa . ' ' . $month_fa;
}

/*-----------------------------
  نمایش داخل صفحه محصول
-----------------------------*/
add_action( 'woocommerce_single_product_summary', 'fzd_ready_single_box', 12 );
function fzd_ready_single_box() {
    if ( ! is_product() ) return;

    $product_id = get_the_ID();
    if ( ! $product_id ) return;

    $product = wc_get_product( $product_id );
    if ( ! $product || ! $product->is_in_stock() ) return;

    $rows = fzd_ready_get_rows( $product_id );
    if ( empty( $rows ) ) return;

    echo '<div style="margin-top:10px;margin-bottom:10px;padding:8px 12px;border:1px solid #e53935;border-radius:6px;font-size:16px;line-height:1.9;">';
    echo '<strong>رنگ‌های آماده تحویل:</strong>';
    echo '<ul style="margin:5px 0 0 0;padding-right:18px;list-style:disc;">';

    foreach ( $rows as $row ) {
        $color = isset( $row['color'] ) ? $row['color'] : '';
        $days  = isset( $row['days'] )  ? (int) $row['days'] : 1;
        if ( $color === '' ) continue;
        if ( $days < 1 ) $days = 1;

        $ts   = current_time( 'timestamp' ) + $days * DAY_IN_SECONDS;
        $date = fzd_ready_convert_jalali( $ts );
        $days_fa = fzd_ready_persian_digits( $days );

        echo '<li>این محصول را در رنگ <strong>' . esc_html( $color ) .
             '</strong> تا <strong>' . esc_html( $date ) .
             '</strong> تحویل بگیرید (حدود ' . $days_fa . ' روزه).</li>';
    }

    echo '</ul>';

    // متن پیش‌فرض – مشکی
    $default_note = 'سایر رنگ‌ها به صورت سفارشی تولید می‌شوند و زمان تحویل آن‌ها کمی بیشتر است؛ پس از ثبت سفارش، زمان دقیق با شما هماهنگ می‌شود.';
    echo '<p style="margin-top:8px;font-size:14px;color:#333333;">' . esc_html( $default_note ) . '</p>';

    // توضیح دستی – سبز
    $note = fzd_ready_get_note( $product_id );
    if ( $note !== '' ) {
        echo '<p style="margin-top:2px;font-size:16px;color:#388e3c;">' . esc_html( $note ) . '</p>';
    }

    echo '</div>';
}/*-----------------------------
  لیبل «آماده تحویل» روی عکس (فلت‌سام)
-----------------------------*/
add_action( 'flatsome_woocommerce_shop_loop_images', 'fzd_ready_badge', 20 );
function fzd_ready_badge() {
    global $product;
    if ( ! $product || ! is_a( $product, 'WC_Product' ) ) return;
    if ( ! $product->is_in_stock() ) return;

    $rows = fzd_ready_get_rows( $product->get_id() );
    if ( empty( $rows ) ) return;

    // استایل فقط یک بار چاپ شود
    static $printed = false;
    if ( ! $printed ) {
    echo '<style>
        .product-small .box-image { position: relative; }

        /* لیبل آماده تحویل – گوشه بالا راست */
        .product-small .box-image .fzd-ready-badge {
            position: absolute;
            top: -2px;
            right: 8px;
            z-index: 10;
        }

        /* بادج تخفیف فلت‌سام – بیاد گوشه بالا چپ */
        .product-small .box-image .badge-container {
            left: 8px;
            right: auto;
        }
    </style>';
    $printed = true;
}

    echo '<span class="fzd-ready-badge" style="display:inline-block;background:#e53935;color:#ffffff;padding:3px 10px;border-radius:16px;font-size:14px;">آماده تحویل</span>';
}

/*-----------------------------
  متن تحویل + توضیح سبز در لیست محصولات
-----------------------------*/
add_action( 'woocommerce_after_shop_loop_item_title', 'fzd_ready_loop_text', 15 );
function fzd_ready_loop_text() {
    global $product;
    if ( ! $product || ! is_a( $product, 'WC_Product' ) ) return;
    if ( ! $product->is_in_stock() ) return;

    $rows = fzd_ready_get_rows( $product->get_id() );
    if ( empty( $rows ) ) return;

    $max   = 2; // حداکثر دو رنگ در دسته‌بندی
    $count = 0;

    // متن قرمز زیر محصول
    echo '<div style="margin-top:4px;font-size:14px;color:#c62828;line-height:1.7;">';

    foreach ( $rows as $row ) {
        if ( $count >= $max ) break;

        $color = isset( $row['color'] ) ? $row['color'] : '';
        $days  = isset( $row['days'] )  ? (int) $row['days'] : 1;
        if ( $color === '' ) continue;
        if ( $days < 1 ) $days = 1;

        $ts   = current_time( 'timestamp' ) + $days * DAY_IN_SECONDS;
        $date = fzd_ready_convert_jalali( $ts );

        echo 'رنگ ' . esc_html( $color ) . ' را تا ' . esc_html( $date ) . ' تحویل بگیرید<br>';

        $count++;
    }

    echo '</div>';

    // توضیح دستی سبز
    $note = fzd_ready_get_note( $product->get_id() );
    if ( $note !== '' ) {
        echo '<div style="margin-top:2px;font-size:14px;color:#388e3c;line-height:1.6;">' . esc_html( $note ) . '</div>';
    }
}
واتساپ ۲
TEXT - 2026-06-07 23:30:14
<?php // بنر واتساپ / اینستاگرام / محصولات آماده تحویل – با نگه‌داشتن وضعیت به مدت ۲۴ ساعت add_action( 'wp_footer', 'fz_quick_contact_banner' ); function fz_quick_contact_banner() { // نمایش ندادن بنر در آدرس‌های مشخص‌شده $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; $path = parse_url( $request_uri, PHP_URL_PATH ); $path = rawurldecode( (string) $path ); $path = trim( $path, "/ \t\n\r\0\x0B" ); $path = strtolower( $path ); // هر آدرسی که اینجا باشد، بنر در آن صفحه نمایش داده نمی‌شود $blocked_slugs = array( 'sef', 'faryazan-orders', 'kar', ); foreach ( $blocked_slugs as $slug ) { $slug = trim( strtolower( $slug ), '/' ); if ( preg_match( '#(^|/)' . preg_quote( $slug, '#' ) . '(/|$)#u', $path ) ) { return; } } // روش وردپرسی برای اطمینان بیشتر if ( function_exists( 'is_page' ) && is_page( $blocked_slugs ) ) { return; } if ( function_exists( 'is_cart' ) && is_cart() ) { return; } if ( function_exists( 'is_checkout' ) && is_checkout() ) { return; } ?> <style> #fz-quick-contact { position: fixed; left: 8px; bottom: 16px; z-index: 9999; font-size: 12px; } #fz-quick-contact .fz-stack { position: relative; display: flex; flex-direction: column; gap: 6px; } #fz-quick-contact .fz-group-btn { position: absolute; top: -24px; left: 0; background: #fff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 0 8px; height: 22px; font-size: 11px; display: flex; align-items: center; gap: 4px; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.08); } #fz-quick-contact .fz-group-btn span { font-size: 13px; } #fz-quick-contact .fz-item { display: flex; align-items: center; direction: ltr; column-gap: 4px; } #fz-quick-contact .fz-link { flex: 0; text-decoration: none; color: inherit; display: inline-flex; } #fz-quick-contact .fz-card .fz-link { flex: 1; display: flex; } #fz-quick-contact .fz-inner { display: flex; flex-direction: row; align-items: center; gap: 8px; } #fz-quick-contact .fz-icon { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } #fz-quick-contact .fz-icon svg { width: 16px; height: 16px; } #fz-quick-contact .fz-icon-wa { background: #25d366; } #fz-quick-contact .fz-icon-ig { background: #c13584; } #fz-quick-contact .fz-icon-ready { background: #e53935; } #fz-quick-contact .fz-text { direction: rtl; text-align: right; line-height: 1.4; white-space: nowrap; } #fz-quick-contact .fz-title { font-weight: 700; font-size: 13px; } #fz-quick-contact .fz-sub { font-size: 11px; color: #777; } #fz-quick-contact .fz-wa .fz-title { color: #1e9f4d; } #fz-quick-contact .fz-ig .fz-title { color: #c13584; } #fz-quick-contact .fz-ready .fz-title { color: #e53935; text-align: center; } #fz-quick-contact .fz-ready .fz-sub { text-align: center; } #fz-quick-contact .fz-close { background: transparent; border: none; font-size: 16px; line-height: 1; cursor: pointer; padding: 0; color: #666; margin-left: 0; } #fz-quick-contact .fz-line .fz-link { background: #ffffff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 4px 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.06); max-width: 260px; } #fz-quick-contact .fz-card { padding: 6px 10px; border-radius: 8px; border: 1px solid #e3e3e3; box-shadow: 0 4px 10px rgba(0,0,0,0.05); background: #ffffff; } #fz-quick-contact .fz-mini { display: none; align-items: center; gap: 6px; background: #ffffff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 4px 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.08); margin-top: 4px; } #fz-quick-contact .fz-mini-icon { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; text-decoration: none; } #fz-quick-contact .fz-mini-icon svg { width: 16px; height: 16px; } #fz-quick-contact .fz-mini-wa { background: #25d366; } #fz-quick-contact .fz-mini-ig { background: #c13584; } #fz-quick-contact .fz-mini-ready { background: #e53935; } #fz-quick-contact .fz-mini-btn { border: none; background: transparent; cursor: pointer; font-size: 16px; line-height: 1; padding: 0 4px; color: #555; } #fz-quick-contact.is-minimized .fz-stack { display: none; } #fz-quick-contact.is-minimized .fz-group-btn { display: none; } #fz-quick-contact.is-minimized .fz-mini { display: flex; } @media (max-width: 480px) { #fz-quick-contact { font-size: 11px; bottom: 14px; } } </style> <div id="fz-quick-contact"> <div class="fz-stack"> <button type="button" class="fz-group-btn"> <span>×</span> <small>جمع کردن</small> </button> <div class="fz-item fz-line fz-wa"> <a class="fz-link" href="https://wa.me/989016161821" target="_blank" rel="noopener"> <div class="fz-inner"> <span class="fz-icon fz-icon-wa"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/> <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/> </svg> </span> <span class="fz-text"> <span class="fz-title">واتساپ</span><br> <span class="fz-sub">چت سریع</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> <div class="fz-item fz-line fz-ig"> <a class="fz-link" href="https://www.instagram.com/faryazandecor/" target="_blank" rel="noopener"> <div class="fz-inner"> <span class="fz-icon fz-icon-ig"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/> <circle cx="12" cy="12" r="4" fill="#c13584"/> <circle cx="17" cy="7" r="1" fill="#c13584"/> </svg> </span> <span class="fz-text"> <span class="fz-title">اینستاگرام</span><br> <span class="fz-sub">پیج فریازان</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> <div class="fz-item fz-card fz-ready"> <a class="fz-link" href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/"> <div class="fz-inner"> <span class="fz-icon fz-icon-ready"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="2" y="9" width="11" height="6" fill="#fff"/> <rect x="13" y="10" width="6" height="5" fill="#fff"/> <circle cx="7" cy="17" r="2" fill="#fff"/> <circle cx="16" cy="17" r="2" fill="#fff"/> </svg> </span> <span class="fz-text" style="text-align:center;"> <span class="fz-title">کلیک کنید</span><br> <span class="fz-sub">برای دیدن محصولات آماده تحویل</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> </div> <div class="fz-mini"> <button type="button" class="fz-mini-btn fz-mini-open" aria-label="باز کردن">‹</button> <a href="https://wa.me/989016161821" class="fz-mini-icon fz-mini-wa" target="_blank" rel="noopener"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/> <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/> </svg> </a> <a href="https://www.instagram.com/faryazandecor/" class="fz-mini-icon fz-mini-ig" target="_blank" rel="noopener"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/> <circle cx="12" cy="12" r="4" fill="#c13584"/> <circle cx="17" cy="7" r="1" fill="#c13584"/> </svg> </a> <a href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/" class="fz-mini-icon fz-mini-ready"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="2" y="9" width="11" height="6" fill="#fff"/> <rect x="13" y="10" width="6" height="5" fill="#fff"/> <circle cx="7" cy="17" r="2" fill="#fff"/> <circle cx="16" cy="17" r="2" fill="#fff"/> </svg> </a> <button type="button" class="fz-mini-btn fz-mini-close" aria-label="بستن">×</button> </div> </div> <script> (function () { var root = document.getElementById('fz-quick-contact'); if (!root) return; var STORAGE_KEY = 'fzQuickContactState_v1'; var DAY_MS = 24 * 60 * 60 * 1000; function loadState() { try { var raw = localStorage.getItem(STORAGE_KEY); if (!raw) return {}; var obj = JSON.parse(raw); if (!obj.ts || Date.now() - obj.ts > DAY_MS) { localStorage.removeItem(STORAGE_KEY); return {}; } return obj; } catch (e) { return {}; } } function saveState(state) { state.ts = Date.now(); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} } var state = loadState(); if (state.hidden) { if (root.parentNode) { root.parentNode.removeChild(root); } return; } function applyItemVisibility() { ['wa', 'ig', 'ready'].forEach(function(key) { if (state['hide_' + key]) { var el = root.querySelector('.fz-' + key); if (el) { el.style.display = 'none'; } } }); } applyItemVisibility(); if (state.minimized) { root.classList.add('is-minimized'); } root.querySelectorAll('.fz-close').forEach(function(btn) { btn.addEventListener('click', function(e) { e.preventDefault(); var item = btn.closest('.fz-item'); if (item) { item.style.display = 'none'; if (item.classList.contains('fz-wa')) { state.hide_wa = true; } if (item.classList.contains('fz-ig')) { state.hide_ig = true; } if (item.classList.contains('fz-ready')) { state.hide_ready = true; } saveState(state); } }); }); var groupBtn = root.querySelector('.fz-group-btn'); if (groupBtn) { groupBtn.addEventListener('click', function(e) { e.preventDefault(); root.classList.add('is-minimized'); state.minimized = true; saveState(state); }); } var miniOpen = root.querySelector('.fz-mini-open'); if (miniOpen) { miniOpen.addEventListener('click', function(e) { e.preventDefault(); root.classList.remove('is-minimized'); state.minimized = false; saveState(state); }); } var miniClose = root.querySelector('.fz-mini-close'); if (miniClose) { miniClose.addEventListener('click', function(e) { e.preventDefault(); if (root && root.parentNode) { root.parentNode.removeChild(root); } state.hidden = true; saveState(state); }); } })(); </script> <?php }
<?php
// بنر واتساپ / اینستاگرام / محصولات آماده تحویل – با نگه‌داشتن وضعیت به مدت ۲۴ ساعت

add_action( 'wp_footer', 'fz_quick_contact_banner' );

function fz_quick_contact_banner() {

    // نمایش ندادن بنر در آدرس‌های مشخص‌شده
    $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
    $path        = parse_url( $request_uri, PHP_URL_PATH );
    $path        = rawurldecode( (string) $path );
    $path        = trim( $path, "/ \t\n\r\0\x0B" );
    $path        = strtolower( $path );

    // هر آدرسی که اینجا باشد، بنر در آن صفحه نمایش داده نمی‌شود
    $blocked_slugs = array(
        'sef',
        'faryazan-orders',
        'kar',
    );

    foreach ( $blocked_slugs as $slug ) {
        $slug = trim( strtolower( $slug ), '/' );

        if ( preg_match( '#(^|/)' . preg_quote( $slug, '#' ) . '(/|$)#u', $path ) ) {
            return;
        }
    }

    // روش وردپرسی برای اطمینان بیشتر
    if ( function_exists( 'is_page' ) && is_page( $blocked_slugs ) ) {
        return;
    }

    if ( function_exists( 'is_cart' ) && is_cart() ) {
        return;
    }

    if ( function_exists( 'is_checkout' ) && is_checkout() ) {
        return;
    }
    ?>

    <style>
        #fz-quick-contact {
            position: fixed;
            left: 8px;
            bottom: 16px;
            z-index: 9999;
            font-size: 12px;
        }

        #fz-quick-contact .fz-stack {
            position: relative;
            display: flex;
            flex-direction: column;
            gap: 6px;
        }

        #fz-quick-contact .fz-group-btn {
            position: absolute;
            top: -24px;
            left: 0;
            background: #fff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 0 8px;
            height: 22px;
            font-size: 11px;
            display: flex;
            align-items: center;
            gap: 4px;
            cursor: pointer;
            box-shadow: 0 2px 6px rgba(0,0,0,0.08);
        }

        #fz-quick-contact .fz-group-btn span {
            font-size: 13px;
        }

        #fz-quick-contact .fz-item {
            display: flex;
            align-items: center;
            direction: ltr;
            column-gap: 4px;
        }

        #fz-quick-contact .fz-link {
            flex: 0;
            text-decoration: none;
            color: inherit;
            display: inline-flex;
        }

        #fz-quick-contact .fz-card .fz-link {
            flex: 1;
            display: flex;
        }

        #fz-quick-contact .fz-inner {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 8px;
        }

        #fz-quick-contact .fz-icon {
            width: 24px;
            height: 24px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
        }

        #fz-quick-contact .fz-icon svg {
            width: 16px;
            height: 16px;
        }

        #fz-quick-contact .fz-icon-wa {
            background: #25d366;
        }

        #fz-quick-contact .fz-icon-ig {
            background: #c13584;
        }

        #fz-quick-contact .fz-icon-ready {
            background: #e53935;
        }

        #fz-quick-contact .fz-text {
            direction: rtl;
            text-align: right;
            line-height: 1.4;
            white-space: nowrap;
        }

        #fz-quick-contact .fz-title {
            font-weight: 700;
            font-size: 13px;
        }

        #fz-quick-contact .fz-sub {
            font-size: 11px;
            color: #777;
        }

        #fz-quick-contact .fz-wa .fz-title {
            color: #1e9f4d;
        }

        #fz-quick-contact .fz-ig .fz-title {
            color: #c13584;
        }

        #fz-quick-contact .fz-ready .fz-title {
            color: #e53935;
            text-align: center;
        }

        #fz-quick-contact .fz-ready .fz-sub {
            text-align: center;
        }

        #fz-quick-contact .fz-close {
            background: transparent;
            border: none;
            font-size: 16px;
            line-height: 1;
            cursor: pointer;
            padding: 0;
            color: #666;
            margin-left: 0;
        }

        #fz-quick-contact .fz-line .fz-link {
            background: #ffffff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 4px 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.06);
            max-width: 260px;
        }

        #fz-quick-contact .fz-card {
            padding: 6px 10px;
            border-radius: 8px;
            border: 1px solid #e3e3e3;
            box-shadow: 0 4px 10px rgba(0,0,0,0.05);
            background: #ffffff;
        }

        #fz-quick-contact .fz-mini {
            display: none;
            align-items: center;
            gap: 6px;
            background: #ffffff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 4px 6px;
            box-shadow: 0 2px 6px rgba(0,0,0,0.08);
            margin-top: 4px;
        }

        #fz-quick-contact .fz-mini-icon {
            width: 26px;
            height: 26px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            text-decoration: none;
        }

        #fz-quick-contact .fz-mini-icon svg {
            width: 16px;
            height: 16px;
        }

        #fz-quick-contact .fz-mini-wa {
            background: #25d366;
        }

        #fz-quick-contact .fz-mini-ig {
            background: #c13584;
        }

        #fz-quick-contact .fz-mini-ready {
            background: #e53935;
        }

        #fz-quick-contact .fz-mini-btn {
            border: none;
            background: transparent;
            cursor: pointer;
            font-size: 16px;
            line-height: 1;
            padding: 0 4px;
            color: #555;
        }

        #fz-quick-contact.is-minimized .fz-stack {
            display: none;
        }

        #fz-quick-contact.is-minimized .fz-group-btn {
            display: none;
        }

        #fz-quick-contact.is-minimized .fz-mini {
            display: flex;
        }

        @media (max-width: 480px) {
            #fz-quick-contact {
                font-size: 11px;
                bottom: 14px;
            }
        }
    </style>

    <div id="fz-quick-contact">
        <div class="fz-stack">
            <button type="button" class="fz-group-btn">
                <span>×</span>
                <small>جمع کردن</small>
            </button>

            <div class="fz-item fz-line fz-wa">
                <a class="fz-link" href="https://wa.me/989016161821" target="_blank" rel="noopener">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-wa">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/>
                                <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/>
                            </svg>
                        </span>
                        <span class="fz-text">
                            <span class="fz-title">واتساپ</span><br>
                            <span class="fz-sub">چت سریع</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>

            <div class="fz-item fz-line fz-ig">
                <a class="fz-link" href="https://www.instagram.com/faryazandecor/" target="_blank" rel="noopener">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-ig">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/>
                                <circle cx="12" cy="12" r="4" fill="#c13584"/>
                                <circle cx="17" cy="7" r="1" fill="#c13584"/>
                            </svg>
                        </span>
                        <span class="fz-text">
                            <span class="fz-title">اینستاگرام</span><br>
                            <span class="fz-sub">پیج فریازان</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>

            <div class="fz-item fz-card fz-ready">
                <a class="fz-link" href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-ready">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <rect x="2" y="9" width="11" height="6" fill="#fff"/>
                                <rect x="13" y="10" width="6" height="5" fill="#fff"/>
                                <circle cx="7" cy="17" r="2" fill="#fff"/>
                                <circle cx="16" cy="17" r="2" fill="#fff"/>
                            </svg>
                        </span>
                        <span class="fz-text" style="text-align:center;">
                            <span class="fz-title">کلیک کنید</span><br>
                            <span class="fz-sub">برای دیدن محصولات آماده تحویل</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>
        </div>

        <div class="fz-mini">
            <button type="button" class="fz-mini-btn fz-mini-open" aria-label="باز کردن">‹</button>

            <a href="https://wa.me/989016161821" class="fz-mini-icon fz-mini-wa" target="_blank" rel="noopener">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/>
                    <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/>
                </svg>
            </a>

            <a href="https://www.instagram.com/faryazandecor/" class="fz-mini-icon fz-mini-ig" target="_blank" rel="noopener">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/>
                    <circle cx="12" cy="12" r="4" fill="#c13584"/>
                    <circle cx="17" cy="7" r="1" fill="#c13584"/>
                </svg>
            </a>

            <a href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/" class="fz-mini-icon fz-mini-ready">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <rect x="2" y="9" width="11" height="6" fill="#fff"/>
                    <rect x="13" y="10" width="6" height="5" fill="#fff"/>
                    <circle cx="7" cy="17" r="2" fill="#fff"/>
                    <circle cx="16" cy="17" r="2" fill="#fff"/>
                </svg>
            </a>

            <button type="button" class="fz-mini-btn fz-mini-close" aria-label="بستن">×</button>
        </div>
    </div>

    <script>
        (function () {
            var root = document.getElementById('fz-quick-contact');
            if (!root) return;

            var STORAGE_KEY = 'fzQuickContactState_v1';
            var DAY_MS = 24 * 60 * 60 * 1000;

            function loadState() {
                try {
                    var raw = localStorage.getItem(STORAGE_KEY);
                    if (!raw) return {};

                    var obj = JSON.parse(raw);

                    if (!obj.ts || Date.now() - obj.ts > DAY_MS) {
                        localStorage.removeItem(STORAGE_KEY);
                        return {};
                    }

                    return obj;
                } catch (e) {
                    return {};
                }
            }

            function saveState(state) {
                state.ts = Date.now();

                try {
                    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
                } catch (e) {}
            }

            var state = loadState();

            if (state.hidden) {
                if (root.parentNode) {
                    root.parentNode.removeChild(root);
                }
                return;
            }

            function applyItemVisibility() {
                ['wa', 'ig', 'ready'].forEach(function(key) {
                    if (state['hide_' + key]) {
                        var el = root.querySelector('.fz-' + key);
                        if (el) {
                            el.style.display = 'none';
                        }
                    }
                });
            }

            applyItemVisibility();

            if (state.minimized) {
                root.classList.add('is-minimized');
            }

            root.querySelectorAll('.fz-close').forEach(function(btn) {
                btn.addEventListener('click', function(e) {
                    e.preventDefault();

                    var item = btn.closest('.fz-item');

                    if (item) {
                        item.style.display = 'none';

                        if (item.classList.contains('fz-wa')) {
                            state.hide_wa = true;
                        }

                        if (item.classList.contains('fz-ig')) {
                            state.hide_ig = true;
                        }

                        if (item.classList.contains('fz-ready')) {
                            state.hide_ready = true;
                        }

                        saveState(state);
                    }
                });
            });

            var groupBtn = root.querySelector('.fz-group-btn');

            if (groupBtn) {
                groupBtn.addEventListener('click', function(e) {
                    e.preventDefault();

                    root.classList.add('is-minimized');
                    state.minimized = true;

                    saveState(state);
                });
            }

            var miniOpen = root.querySelector('.fz-mini-open');

            if (miniOpen) {
                miniOpen.addEventListener('click', function(e) {
                    e.preventDefault();

                    root.classList.remove('is-minimized');
                    state.minimized = false;

                    saveState(state);
                });
            }

            var miniClose = root.querySelector('.fz-mini-close');

            if (miniClose) {
                miniClose.addEventListener('click', function(e) {
                    e.preventDefault();

                    if (root && root.parentNode) {
                        root.parentNode.removeChild(root);
                    }

                    state.hidden = true;

                    saveState(state);
                });
            }
        })();
    </script>

    <?php
}
کد وات ساپ
TEXT - 2026-06-07 23:29:58
<?php // بنر واتساپ / اینستاگرام / محصولات آماده تحویل – با نگه‌داشتن وضعیت به مدت ۲۴ ساعت add_action( 'wp_footer', 'fz_quick_contact_banner' ); function fz_quick_contact_banner() { // نمایش ندادن بنر در صفحه اپ سفارشات $fz_path = trim( parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH ), '/' ); if ( in_array( $fz_path, array( 'sef', 'faryazan-orders' ), true ) ) { return; } if ( function_exists('is_cart') && is_cart() ) return; if ( function_exists('is_checkout') && is_checkout() ) return; ?> <style> #fz-quick-contact { position: fixed; left: 8px; bottom: 16px; z-index: 9999; font-size: 12px; } #fz-quick-contact .fz-stack { position: relative; display: flex; flex-direction: column; gap: 6px; } #fz-quick-contact .fz-group-btn { position: absolute; top: -24px; left: 0; background: #fff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 0 8px; height: 22px; font-size: 11px; display: flex; align-items: center; gap: 4px; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.08); } #fz-quick-contact .fz-group-btn span { font-size: 13px; } #fz-quick-contact .fz-item { display: flex; align-items: center; direction: ltr; column-gap: 4px; } #fz-quick-contact .fz-link { flex: 0; text-decoration: none; color: inherit; display: inline-flex; } #fz-quick-contact .fz-card .fz-link { flex: 1; display: flex; } #fz-quick-contact .fz-inner { display: flex; flex-direction: row; align-items: center; gap: 8px; } #fz-quick-contact .fz-icon { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } #fz-quick-contact .fz-icon svg { width: 16px; height: 16px; } #fz-quick-contact .fz-icon-wa { background:#25d366; } #fz-quick-contact .fz-icon-ig { background:#c13584; } #fz-quick-contact .fz-icon-ready { background:#e53935; } #fz-quick-contact .fz-text { direction: rtl; text-align: right; line-height: 1.4; white-space: nowrap; } #fz-quick-contact .fz-title { font-weight: 700; font-size: 13px; } #fz-quick-contact .fz-sub { font-size: 11px; color: #777; } #fz-quick-contact .fz-wa .fz-title { color:#1e9f4d; } #fz-quick-contact .fz-ig .fz-title { color:#c13584; } #fz-quick-contact .fz-ready .fz-title { color:#e53935; text-align:center; } #fz-quick-contact .fz-ready .fz-sub { text-align:center; } #fz-quick-contact .fz-close { background: transparent; border: none; font-size: 16px; line-height: 1; cursor: pointer; padding: 0; color: #666; margin-left: 0; } #fz-quick-contact .fz-line .fz-link { background: #ffffff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 4px 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.06); max-width: 260px; } #fz-quick-contact .fz-card { padding: 6px 10px; border-radius: 8px; border: 1px solid #e3e3e3; box-shadow: 0 4px 10px rgba(0,0,0,0.05); background: #ffffff; } #fz-quick-contact .fz-mini { display: none; align-items: center; gap: 6px; background: #ffffff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 4px 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.08); margin-top: 4px; } #fz-quick-contact .fz-mini-icon { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; text-decoration: none; } #fz-quick-contact .fz-mini-icon svg { width: 16px; height: 16px; } #fz-quick-contact .fz-mini-wa { background:#25d366; } #fz-quick-contact .fz-mini-ig { background:#c13584; } #fz-quick-contact .fz-mini-ready { background:#e53935; } #fz-quick-contact .fz-mini-btn { border: none; background: transparent; cursor: pointer; font-size: 16px; line-height: 1; padding: 0 4px; color: #555; } #fz-quick-contact.is-minimized .fz-stack { display: none; } #fz-quick-contact.is-minimized .fz-group-btn { display: none; } #fz-quick-contact.is-minimized .fz-mini { display: flex; } @media (max-width:480px){ #fz-quick-contact { font-size: 11px; bottom: 14px; } } </style> <div id="fz-quick-contact"> <div class="fz-stack"> <button type="button" class="fz-group-btn"> <span>×</span> <small>جمع کردن</small> </button> <div class="fz-item fz-line fz-wa"> <a class="fz-link" href="https://wa.me/989016161821"> <div class="fz-inner"> <span class="fz-icon fz-icon-wa"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/> <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/> </svg> </span> <span class="fz-text"> <span class="fz-title">واتساپ</span><br> <span class="fz-sub">چت سریع</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> <div class="fz-item fz-line fz-ig"> <a class="fz-link" href="https://www.instagram.com/faryazandecor/"> <div class="fz-inner"> <span class="fz-icon fz-icon-ig"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/> <circle cx="12" cy="12" r="4" fill="#c13584"/> <circle cx="17" cy="7" r="1" fill="#c13584"/> </svg> </span> <span class="fz-text"> <span class="fz-title">اینستاگرام</span><br> <span class="fz-sub">پیج فریازان</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> <div class="fz-item fz-card fz-ready"> <a class="fz-link" href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/"> <div class="fz-inner"> <span class="fz-icon fz-icon-ready"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="2" y="9" width="11" height="6" fill="#fff"/> <rect x="13" y="10" width="6" height="5" fill="#fff"/> <circle cx="7" cy="17" r="2" fill="#fff"/> <circle cx="16" cy="17" r="2" fill="#fff"/> </svg> </span> <span class="fz-text" style="text-align:center;"> <span class="fz-title">کلیک کنید</span><br> <span class="fz-sub">برای دیدن محصولات آماده تحویل</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> </div> <div class="fz-mini"> <button type="button" class="fz-mini-btn fz-mini-open" aria-label="باز کردن">‹</button> <a href="https://wa.me/989016161821" class="fz-mini-icon fz-mini-wa"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/> <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/> </svg> </a> <a href="https://www.instagram.com/faryazandecor/" class="fz-mini-icon fz-mini-ig"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/> <circle cx="12" cy="12" r="4" fill="#c13584"/> <circle cx="17" cy="7" r="1" fill="#c13584"/> </svg> </a> <a href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/" class="fz-mini-icon fz-mini-ready"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="2" y="9" width="11" height="6" fill="#fff"/> <rect x="13" y="10" width="6" height="5" fill="#fff"/> <circle cx="7" cy="17" r="2" fill="#fff"/> <circle cx="16" cy="17" r="2" fill="#fff"/> </svg> </a> <button type="button" class="fz-mini-btn fz-mini-close" aria-label="بستن">×</button> </div> </div> <script> (function () { var root = document.getElementById('fz-quick-contact'); if (!root) return; var STORAGE_KEY = 'fzQuickContactState_v1'; var DAY_MS = 24 * 60 * 60 * 1000; function loadState() { try { var raw = localStorage.getItem(STORAGE_KEY); if (!raw) return {}; var obj = JSON.parse(raw); if (!obj.ts || Date.now() - obj.ts > DAY_MS) { localStorage.removeItem(STORAGE_KEY); return {}; } return obj; } catch (e) { return {}; } } function saveState(state) { state.ts = Date.now(); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} } var state = loadState(); if (state.hidden) { if (root.parentNode) { root.parentNode.removeChild(root); } return; } function applyItemVisibility() { ['wa', 'ig', 'ready'].forEach(function(key){ if (state['hide_' + key]) { var el = root.querySelector('.fz-' + key); if (el) el.style.display = 'none'; } }); } applyItemVisibility(); if (state.minimized) { root.classList.add('is-minimized'); } root.querySelectorAll('.fz-close').forEach(function(btn){ btn.addEventListener('click', function(e){ e.preventDefault(); var item = btn.closest('.fz-item'); if (item) { item.style.display = 'none'; if (item.classList.contains('fz-wa')) state.hide_wa = true; if (item.classList.contains('fz-ig')) state.hide_ig = true; if (item.classList.contains('fz-ready')) state.hide_ready = true; saveState(state); } }); }); var groupBtn = root.querySelector('.fz-group-btn'); if (groupBtn) { groupBtn.addEventListener('click', function(e){ e.preventDefault(); root.classList.add('is-minimized'); state.minimized = true; saveState(state); }); } var miniOpen = root.querySelector('.fz-mini-open'); if (miniOpen) { miniOpen.addEventListener('click', function(e){ e.preventDefault(); root.classList.remove('is-minimized'); state.minimized = false; saveState(state); }); } var miniClose = root.querySelector('.fz-mini-close'); if (miniClose) { miniClose.addEventListener('click', function(e){ e.preventDefault(); if (root && root.parentNode) { root.parentNode.removeChild(root); } state.hidden = true; saveState(state); }); } })(); </script> <?php }
<?php
// بنر واتساپ / اینستاگرام / محصولات آماده تحویل – با نگه‌داشتن وضعیت به مدت ۲۴ ساعت

add_action( 'wp_footer', 'fz_quick_contact_banner' );

function fz_quick_contact_banner() {

    // نمایش ندادن بنر در صفحه اپ سفارشات
    $fz_path = trim( parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH ), '/' );

    if ( in_array( $fz_path, array( 'sef', 'faryazan-orders' ), true ) ) {
        return;
    }

    if ( function_exists('is_cart') && is_cart() ) return;
    if ( function_exists('is_checkout') && is_checkout() ) return;
    ?>
    <style>
        #fz-quick-contact {
            position: fixed;
            left: 8px;
            bottom: 16px;
            z-index: 9999;
            font-size: 12px;
        }
        #fz-quick-contact .fz-stack {
            position: relative;
            display: flex;
            flex-direction: column;
            gap: 6px;
        }

        #fz-quick-contact .fz-group-btn {
            position: absolute;
            top: -24px;
            left: 0;
            background: #fff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 0 8px;
            height: 22px;
            font-size: 11px;
            display: flex;
            align-items: center;
            gap: 4px;
            cursor: pointer;
            box-shadow: 0 2px 6px rgba(0,0,0,0.08);
        }
        #fz-quick-contact .fz-group-btn span {
            font-size: 13px;
        }

        #fz-quick-contact .fz-item {
            display: flex;
            align-items: center;
            direction: ltr;
            column-gap: 4px;
        }

        #fz-quick-contact .fz-link {
            flex: 0;
            text-decoration: none;
            color: inherit;
            display: inline-flex;
        }

        #fz-quick-contact .fz-card .fz-link {
            flex: 1;
            display: flex;
        }

        #fz-quick-contact .fz-inner {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 8px;
        }

        #fz-quick-contact .fz-icon {
            width: 24px;
            height: 24px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
        }
        #fz-quick-contact .fz-icon svg {
            width: 16px;
            height: 16px;
        }
        #fz-quick-contact .fz-icon-wa { background:#25d366; }
        #fz-quick-contact .fz-icon-ig { background:#c13584; }
        #fz-quick-contact .fz-icon-ready { background:#e53935; }

        #fz-quick-contact .fz-text {
            direction: rtl;
            text-align: right;
            line-height: 1.4;
            white-space: nowrap;
        }
        #fz-quick-contact .fz-title {
            font-weight: 700;
            font-size: 13px;
        }
        #fz-quick-contact .fz-sub {
            font-size: 11px;
            color: #777;
        }
        #fz-quick-contact .fz-wa .fz-title { color:#1e9f4d; }
        #fz-quick-contact .fz-ig .fz-title { color:#c13584; }
        #fz-quick-contact .fz-ready .fz-title { color:#e53935; text-align:center; }
        #fz-quick-contact .fz-ready .fz-sub { text-align:center; }

        #fz-quick-contact .fz-close {
            background: transparent;
            border: none;
            font-size: 16px;
            line-height: 1;
            cursor: pointer;
            padding: 0;
            color: #666;
            margin-left: 0;
        }

        #fz-quick-contact .fz-line .fz-link {
            background: #ffffff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 4px 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.06);
            max-width: 260px;
        }

        #fz-quick-contact .fz-card {
            padding: 6px 10px;
            border-radius: 8px;
            border: 1px solid #e3e3e3;
            box-shadow: 0 4px 10px rgba(0,0,0,0.05);
            background: #ffffff;
        }

        #fz-quick-contact .fz-mini {
            display: none;
            align-items: center;
            gap: 6px;
            background: #ffffff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 4px 6px;
            box-shadow: 0 2px 6px rgba(0,0,0,0.08);
            margin-top: 4px;
        }
        #fz-quick-contact .fz-mini-icon {
            width: 26px;
            height: 26px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            text-decoration: none;
        }
        #fz-quick-contact .fz-mini-icon svg {
            width: 16px;
            height: 16px;
        }
        #fz-quick-contact .fz-mini-wa { background:#25d366; }
        #fz-quick-contact .fz-mini-ig { background:#c13584; }
        #fz-quick-contact .fz-mini-ready { background:#e53935; }

        #fz-quick-contact .fz-mini-btn {
            border: none;
            background: transparent;
            cursor: pointer;
            font-size: 16px;
            line-height: 1;
            padding: 0 4px;
            color: #555;
        }

        #fz-quick-contact.is-minimized .fz-stack {
            display: none;
        }
        #fz-quick-contact.is-minimized .fz-group-btn {
            display: none;
        }
        #fz-quick-contact.is-minimized .fz-mini {
            display: flex;
        }

        @media (max-width:480px){
            #fz-quick-contact {
                font-size: 11px;
                bottom: 14px;
            }
        }
    </style>

    <div id="fz-quick-contact">
        <div class="fz-stack">
            <button type="button" class="fz-group-btn">
                <span>×</span>
                <small>جمع کردن</small>
            </button>

            <div class="fz-item fz-line fz-wa">
                <a class="fz-link" href="https://wa.me/989016161821">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-wa">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/>
                                <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/>
                            </svg>
                        </span>
                        <span class="fz-text">
                            <span class="fz-title">واتساپ</span><br>
                            <span class="fz-sub">چت سریع</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>

            <div class="fz-item fz-line fz-ig">
                <a class="fz-link" href="https://www.instagram.com/faryazandecor/">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-ig">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/>
                                <circle cx="12" cy="12" r="4" fill="#c13584"/>
                                <circle cx="17" cy="7" r="1" fill="#c13584"/>
                            </svg>
                        </span>
                        <span class="fz-text">
                            <span class="fz-title">اینستاگرام</span><br>
                            <span class="fz-sub">پیج فریازان</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>

            <div class="fz-item fz-card fz-ready">
                <a class="fz-link" href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-ready">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <rect x="2" y="9" width="11" height="6" fill="#fff"/>
                                <rect x="13" y="10" width="6" height="5" fill="#fff"/>
                                <circle cx="7" cy="17" r="2" fill="#fff"/>
                                <circle cx="16" cy="17" r="2" fill="#fff"/>
                            </svg>
                        </span>
                        <span class="fz-text" style="text-align:center;">
                            <span class="fz-title">کلیک کنید</span><br>
                            <span class="fz-sub">برای دیدن محصولات آماده تحویل</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>
        </div>

        <div class="fz-mini">
            <button type="button" class="fz-mini-btn fz-mini-open" aria-label="باز کردن">‹</button>

            <a href="https://wa.me/989016161821" class="fz-mini-icon fz-mini-wa">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/>
                    <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/>
                </svg>
            </a>

            <a href="https://www.instagram.com/faryazandecor/" class="fz-mini-icon fz-mini-ig">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/>
                    <circle cx="12" cy="12" r="4" fill="#c13584"/>
                    <circle cx="17" cy="7" r="1" fill="#c13584"/>
                </svg>
            </a>

            <a href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/" class="fz-mini-icon fz-mini-ready">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <rect x="2" y="9" width="11" height="6" fill="#fff"/>
                    <rect x="13" y="10" width="6" height="5" fill="#fff"/>
                    <circle cx="7" cy="17" r="2" fill="#fff"/>
                    <circle cx="16" cy="17" r="2" fill="#fff"/>
                </svg>
            </a>

            <button type="button" class="fz-mini-btn fz-mini-close" aria-label="بستن">×</button>
        </div>
    </div>

    <script>
        (function () {
            var root = document.getElementById('fz-quick-contact');
            if (!root) return;

            var STORAGE_KEY = 'fzQuickContactState_v1';
            var DAY_MS = 24 * 60 * 60 * 1000;

            function loadState() {
                try {
                    var raw = localStorage.getItem(STORAGE_KEY);
                    if (!raw) return {};
                    var obj = JSON.parse(raw);
                    if (!obj.ts || Date.now() - obj.ts > DAY_MS) {
                        localStorage.removeItem(STORAGE_KEY);
                        return {};
                    }
                    return obj;
                } catch (e) {
                    return {};
                }
            }

            function saveState(state) {
                state.ts = Date.now();
                try {
                    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
                } catch (e) {}
            }

            var state = loadState();

            if (state.hidden) {
                if (root.parentNode) {
                    root.parentNode.removeChild(root);
                }
                return;
            }

            function applyItemVisibility() {
                ['wa', 'ig', 'ready'].forEach(function(key){
                    if (state['hide_' + key]) {
                        var el = root.querySelector('.fz-' + key);
                        if (el) el.style.display = 'none';
                    }
                });
            }
            applyItemVisibility();

            if (state.minimized) {
                root.classList.add('is-minimized');
            }

            root.querySelectorAll('.fz-close').forEach(function(btn){
                btn.addEventListener('click', function(e){
                    e.preventDefault();
                    var item = btn.closest('.fz-item');
                    if (item) {
                        item.style.display = 'none';

                        if (item.classList.contains('fz-wa'))    state.hide_wa    = true;
                        if (item.classList.contains('fz-ig'))    state.hide_ig    = true;
                        if (item.classList.contains('fz-ready')) state.hide_ready = true;

                        saveState(state);
                    }
                });
            });

            var groupBtn = root.querySelector('.fz-group-btn');
            if (groupBtn) {
                groupBtn.addEventListener('click', function(e){
                    e.preventDefault();
                    root.classList.add('is-minimized');
                    state.minimized = true;
                    saveState(state);
                });
            }

            var miniOpen = root.querySelector('.fz-mini-open');
            if (miniOpen) {
                miniOpen.addEventListener('click', function(e){
                    e.preventDefault();
                    root.classList.remove('is-minimized');
                    state.minimized = false;
                    saveState(state);
                });
            }

            var miniClose = root.querySelector('.fz-mini-close');
            if (miniClose) {
                miniClose.addEventListener('click', function(e){
                    e.preventDefault();
                    if (root && root.parentNode) {
                        root.parentNode.removeChild(root);
                    }
                    state.hidden = true;
                    saveState(state);
                });
            }
        })();
    </script>
    <?php
}
کد وات ساپ
TEXT - 2026-06-07 23:21:35
<?php // بنر واتساپ / اینستاگرام / محصولات آماده تحویل – با نگه‌داشتن وضعیت به مدت ۲۴ ساعت add_action( 'wp_footer', 'fz_quick_contact_banner' ); function fz_quick_contact_banner() { // نمایش ندادن بنر در صفحه اپ سفارشات $fz_path = trim( parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH ), '/' ); if ( in_array( $fz_path, array( 'sef', 'faryazan-orders' ), true ) ) { return; } if ( function_exists('is_cart') && is_cart() ) return; if ( function_exists('is_checkout') && is_checkout() ) return; ?> <style> #fz-quick-contact { position: fixed; left: 8px; bottom: 16px; z-index: 9999; font-size: 12px; } #fz-quick-contact .fz-stack { position: relative; display: flex; flex-direction: column; gap: 6px; } #fz-quick-contact .fz-group-btn { position: absolute; top: -24px; left: 0; background: #fff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 0 8px; height: 22px; font-size: 11px; display: flex; align-items: center; gap: 4px; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.08); } #fz-quick-contact .fz-group-btn span { font-size: 13px; } #fz-quick-contact .fz-item { display: flex; align-items: center; direction: ltr; column-gap: 4px; } #fz-quick-contact .fz-link { flex: 0; text-decoration: none; color: inherit; display: inline-flex; } #fz-quick-contact .fz-card .fz-link { flex: 1; display: flex; } #fz-quick-contact .fz-inner { display: flex; flex-direction: row; align-items: center; gap: 8px; } #fz-quick-contact .fz-icon { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } #fz-quick-contact .fz-icon svg { width: 16px; height: 16px; } #fz-quick-contact .fz-icon-wa { background:#25d366; } #fz-quick-contact .fz-icon-ig { background:#c13584; } #fz-quick-contact .fz-icon-ready { background:#e53935; } #fz-quick-contact .fz-text { direction: rtl; text-align: right; line-height: 1.4; white-space: nowrap; } #fz-quick-contact .fz-title { font-weight: 700; font-size: 13px; } #fz-quick-contact .fz-sub { font-size: 11px; color: #777; } #fz-quick-contact .fz-wa .fz-title { color:#1e9f4d; } #fz-quick-contact .fz-ig .fz-title { color:#c13584; } #fz-quick-contact .fz-ready .fz-title { color:#e53935; text-align:center; } #fz-quick-contact .fz-ready .fz-sub { text-align:center; } #fz-quick-contact .fz-close { background: transparent; border: none; font-size: 16px; line-height: 1; cursor: pointer; padding: 0; color: #666; margin-left: 0; } #fz-quick-contact .fz-line .fz-link { background: #ffffff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 4px 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.06); max-width: 260px; } #fz-quick-contact .fz-card { padding: 6px 10px; border-radius: 8px; border: 1px solid #e3e3e3; box-shadow: 0 4px 10px rgba(0,0,0,0.05); background: #ffffff; } #fz-quick-contact .fz-mini { display: none; align-items: center; gap: 6px; background: #ffffff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 4px 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.08); margin-top: 4px; } #fz-quick-contact .fz-mini-icon { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; text-decoration: none; } #fz-quick-contact .fz-mini-icon svg { width: 16px; height: 16px; } #fz-quick-contact .fz-mini-wa { background:#25d366; } #fz-quick-contact .fz-mini-ig { background:#c13584; } #fz-quick-contact .fz-mini-ready { background:#e53935; } #fz-quick-contact .fz-mini-btn { border: none; background: transparent; cursor: pointer; font-size: 16px; line-height: 1; padding: 0 4px; color: #555; } #fz-quick-contact.is-minimized .fz-stack { display: none; } #fz-quick-contact.is-minimized .fz-group-btn { display: none; } #fz-quick-contact.is-minimized .fz-mini { display: flex; } @media (max-width:480px){ #fz-quick-contact { font-size: 11px; bottom: 14px; } } </style> <div id="fz-quick-contact"> <div class="fz-stack"> <button type="button" class="fz-group-btn"> <span>×</span> <small>جمع کردن</small> </button> <div class="fz-item fz-line fz-wa"> <a class="fz-link" href="https://wa.me/989016161821"> <div class="fz-inner"> <span class="fz-icon fz-icon-wa"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/> <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/> </svg> </span> <span class="fz-text"> <span class="fz-title">واتساپ</span><br> <span class="fz-sub">چت سریع</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> <div class="fz-item fz-line fz-ig"> <a class="fz-link" href="https://www.instagram.com/faryazandecor/"> <div class="fz-inner"> <span class="fz-icon fz-icon-ig"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/> <circle cx="12" cy="12" r="4" fill="#c13584"/> <circle cx="17" cy="7" r="1" fill="#c13584"/> </svg> </span> <span class="fz-text"> <span class="fz-title">اینستاگرام</span><br> <span class="fz-sub">پیج فریازان</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> <div class="fz-item fz-card fz-ready"> <a class="fz-link" href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/"> <div class="fz-inner"> <span class="fz-icon fz-icon-ready"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="2" y="9" width="11" height="6" fill="#fff"/> <rect x="13" y="10" width="6" height="5" fill="#fff"/> <circle cx="7" cy="17" r="2" fill="#fff"/> <circle cx="16" cy="17" r="2" fill="#fff"/> </svg> </span> <span class="fz-text" style="text-align:center;"> <span class="fz-title">کلیک کنید</span><br> <span class="fz-sub">برای دیدن محصولات آماده تحویل</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> </div> <div class="fz-mini"> <button type="button" class="fz-mini-btn fz-mini-open" aria-label="باز کردن">‹</button> <a href="https://wa.me/989016161821" class="fz-mini-icon fz-mini-wa"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/> <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/> </svg> </a> <a href="https://www.instagram.com/faryazandecor/" class="fz-mini-icon fz-mini-ig"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/> <circle cx="12" cy="12" r="4" fill="#c13584"/> <circle cx="17" cy="7" r="1" fill="#c13584"/> </svg> </a> <a href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/" class="fz-mini-icon fz-mini-ready"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="2" y="9" width="11" height="6" fill="#fff"/> <rect x="13" y="10" width="6" height="5" fill="#fff"/> <circle cx="7" cy="17" r="2" fill="#fff"/> <circle cx="16" cy="17" r="2" fill="#fff"/> </svg> </a> <button type="button" class="fz-mini-btn fz-mini-close" aria-label="بستن">×</button> </div> </div> <script> (function () { var root = document.getElementById('fz-quick-contact'); if (!root) return; var STORAGE_KEY = 'fzQuickContactState_v1'; var DAY_MS = 24 * 60 * 60 * 1000; function loadState() { try { var raw = localStorage.getItem(STORAGE_KEY); if (!raw) return {}; var obj = JSON.parse(raw); if (!obj.ts || Date.now() - obj.ts > DAY_MS) { localStorage.removeItem(STORAGE_KEY); return {}; } return obj; } catch (e) { return {}; } } function saveState(state) { state.ts = Date.now(); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} } var state = loadState(); if (state.hidden) { if (root.parentNode) { root.parentNode.removeChild(root); } return; } function applyItemVisibility() { ['wa', 'ig', 'ready'].forEach(function(key){ if (state['hide_' + key]) { var el = root.querySelector('.fz-' + key); if (el) el.style.display = 'none'; } }); } applyItemVisibility(); if (state.minimized) { root.classList.add('is-minimized'); } root.querySelectorAll('.fz-close').forEach(function(btn){ btn.addEventListener('click', function(e){ e.preventDefault(); var item = btn.closest('.fz-item'); if (item) { item.style.display = 'none'; if (item.classList.contains('fz-wa')) state.hide_wa = true; if (item.classList.contains('fz-ig')) state.hide_ig = true; if (item.classList.contains('fz-ready')) state.hide_ready = true; saveState(state); } }); }); var groupBtn = root.querySelector('.fz-group-btn'); if (groupBtn) { groupBtn.addEventListener('click', function(e){ e.preventDefault(); root.classList.add('is-minimized'); state.minimized = true; saveState(state); }); } var miniOpen = root.querySelector('.fz-mini-open'); if (miniOpen) { miniOpen.addEventListener('click', function(e){ e.preventDefault(); root.classList.remove('is-minimized'); state.minimized = false; saveState(state); }); } var miniClose = root.querySelector('.fz-mini-close'); if (miniClose) { miniClose.addEventListener('click', function(e){ e.preventDefault(); if (root && root.parentNode) { root.parentNode.removeChild(root); } state.hidden = true; saveState(state); }); } })(); </script> <?php }
<?php
// بنر واتساپ / اینستاگرام / محصولات آماده تحویل – با نگه‌داشتن وضعیت به مدت ۲۴ ساعت

add_action( 'wp_footer', 'fz_quick_contact_banner' );

function fz_quick_contact_banner() {

    // نمایش ندادن بنر در صفحه اپ سفارشات
    $fz_path = trim( parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH ), '/' );

    if ( in_array( $fz_path, array( 'sef', 'faryazan-orders' ), true ) ) {
        return;
    }

    if ( function_exists('is_cart') && is_cart() ) return;
    if ( function_exists('is_checkout') && is_checkout() ) return;
    ?>
    <style>
        #fz-quick-contact {
            position: fixed;
            left: 8px;
            bottom: 16px;
            z-index: 9999;
            font-size: 12px;
        }
        #fz-quick-contact .fz-stack {
            position: relative;
            display: flex;
            flex-direction: column;
            gap: 6px;
        }

        #fz-quick-contact .fz-group-btn {
            position: absolute;
            top: -24px;
            left: 0;
            background: #fff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 0 8px;
            height: 22px;
            font-size: 11px;
            display: flex;
            align-items: center;
            gap: 4px;
            cursor: pointer;
            box-shadow: 0 2px 6px rgba(0,0,0,0.08);
        }
        #fz-quick-contact .fz-group-btn span {
            font-size: 13px;
        }

        #fz-quick-contact .fz-item {
            display: flex;
            align-items: center;
            direction: ltr;
            column-gap: 4px;
        }

        #fz-quick-contact .fz-link {
            flex: 0;
            text-decoration: none;
            color: inherit;
            display: inline-flex;
        }

        #fz-quick-contact .fz-card .fz-link {
            flex: 1;
            display: flex;
        }

        #fz-quick-contact .fz-inner {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 8px;
        }

        #fz-quick-contact .fz-icon {
            width: 24px;
            height: 24px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
        }
        #fz-quick-contact .fz-icon svg {
            width: 16px;
            height: 16px;
        }
        #fz-quick-contact .fz-icon-wa { background:#25d366; }
        #fz-quick-contact .fz-icon-ig { background:#c13584; }
        #fz-quick-contact .fz-icon-ready { background:#e53935; }

        #fz-quick-contact .fz-text {
            direction: rtl;
            text-align: right;
            line-height: 1.4;
            white-space: nowrap;
        }
        #fz-quick-contact .fz-title {
            font-weight: 700;
            font-size: 13px;
        }
        #fz-quick-contact .fz-sub {
            font-size: 11px;
            color: #777;
        }
        #fz-quick-contact .fz-wa .fz-title { color:#1e9f4d; }
        #fz-quick-contact .fz-ig .fz-title { color:#c13584; }
        #fz-quick-contact .fz-ready .fz-title { color:#e53935; text-align:center; }
        #fz-quick-contact .fz-ready .fz-sub { text-align:center; }

        #fz-quick-contact .fz-close {
            background: transparent;
            border: none;
            font-size: 16px;
            line-height: 1;
            cursor: pointer;
            padding: 0;
            color: #666;
            margin-left: 0;
        }

        #fz-quick-contact .fz-line .fz-link {
            background: #ffffff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 4px 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.06);
            max-width: 260px;
        }

        #fz-quick-contact .fz-card {
            padding: 6px 10px;
            border-radius: 8px;
            border: 1px solid #e3e3e3;
            box-shadow: 0 4px 10px rgba(0,0,0,0.05);
            background: #ffffff;
        }

        #fz-quick-contact .fz-mini {
            display: none;
            align-items: center;
            gap: 6px;
            background: #ffffff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 4px 6px;
            box-shadow: 0 2px 6px rgba(0,0,0,0.08);
            margin-top: 4px;
        }
        #fz-quick-contact .fz-mini-icon {
            width: 26px;
            height: 26px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            text-decoration: none;
        }
        #fz-quick-contact .fz-mini-icon svg {
            width: 16px;
            height: 16px;
        }
        #fz-quick-contact .fz-mini-wa { background:#25d366; }
        #fz-quick-contact .fz-mini-ig { background:#c13584; }
        #fz-quick-contact .fz-mini-ready { background:#e53935; }

        #fz-quick-contact .fz-mini-btn {
            border: none;
            background: transparent;
            cursor: pointer;
            font-size: 16px;
            line-height: 1;
            padding: 0 4px;
            color: #555;
        }

        #fz-quick-contact.is-minimized .fz-stack {
            display: none;
        }
        #fz-quick-contact.is-minimized .fz-group-btn {
            display: none;
        }
        #fz-quick-contact.is-minimized .fz-mini {
            display: flex;
        }

        @media (max-width:480px){
            #fz-quick-contact {
                font-size: 11px;
                bottom: 14px;
            }
        }
    </style>

    <div id="fz-quick-contact">
        <div class="fz-stack">
            <button type="button" class="fz-group-btn">
                <span>×</span>
                <small>جمع کردن</small>
            </button>

            <div class="fz-item fz-line fz-wa">
                <a class="fz-link" href="https://wa.me/989016161821">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-wa">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/>
                                <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/>
                            </svg>
                        </span>
                        <span class="fz-text">
                            <span class="fz-title">واتساپ</span><br>
                            <span class="fz-sub">چت سریع</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>

            <div class="fz-item fz-line fz-ig">
                <a class="fz-link" href="https://www.instagram.com/faryazandecor/">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-ig">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/>
                                <circle cx="12" cy="12" r="4" fill="#c13584"/>
                                <circle cx="17" cy="7" r="1" fill="#c13584"/>
                            </svg>
                        </span>
                        <span class="fz-text">
                            <span class="fz-title">اینستاگرام</span><br>
                            <span class="fz-sub">پیج فریازان</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>

            <div class="fz-item fz-card fz-ready">
                <a class="fz-link" href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-ready">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <rect x="2" y="9" width="11" height="6" fill="#fff"/>
                                <rect x="13" y="10" width="6" height="5" fill="#fff"/>
                                <circle cx="7" cy="17" r="2" fill="#fff"/>
                                <circle cx="16" cy="17" r="2" fill="#fff"/>
                            </svg>
                        </span>
                        <span class="fz-text" style="text-align:center;">
                            <span class="fz-title">کلیک کنید</span><br>
                            <span class="fz-sub">برای دیدن محصولات آماده تحویل</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>
        </div>

        <div class="fz-mini">
            <button type="button" class="fz-mini-btn fz-mini-open" aria-label="باز کردن">‹</button>

            <a href="https://wa.me/989016161821" class="fz-mini-icon fz-mini-wa">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/>
                    <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/>
                </svg>
            </a>

            <a href="https://www.instagram.com/faryazandecor/" class="fz-mini-icon fz-mini-ig">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/>
                    <circle cx="12" cy="12" r="4" fill="#c13584"/>
                    <circle cx="17" cy="7" r="1" fill="#c13584"/>
                </svg>
            </a>

            <a href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/" class="fz-mini-icon fz-mini-ready">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <rect x="2" y="9" width="11" height="6" fill="#fff"/>
                    <rect x="13" y="10" width="6" height="5" fill="#fff"/>
                    <circle cx="7" cy="17" r="2" fill="#fff"/>
                    <circle cx="16" cy="17" r="2" fill="#fff"/>
                </svg>
            </a>

            <button type="button" class="fz-mini-btn fz-mini-close" aria-label="بستن">×</button>
        </div>
    </div>

    <script>
        (function () {
            var root = document.getElementById('fz-quick-contact');
            if (!root) return;

            var STORAGE_KEY = 'fzQuickContactState_v1';
            var DAY_MS = 24 * 60 * 60 * 1000;

            function loadState() {
                try {
                    var raw = localStorage.getItem(STORAGE_KEY);
                    if (!raw) return {};
                    var obj = JSON.parse(raw);
                    if (!obj.ts || Date.now() - obj.ts > DAY_MS) {
                        localStorage.removeItem(STORAGE_KEY);
                        return {};
                    }
                    return obj;
                } catch (e) {
                    return {};
                }
            }

            function saveState(state) {
                state.ts = Date.now();
                try {
                    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
                } catch (e) {}
            }

            var state = loadState();

            if (state.hidden) {
                if (root.parentNode) {
                    root.parentNode.removeChild(root);
                }
                return;
            }

            function applyItemVisibility() {
                ['wa', 'ig', 'ready'].forEach(function(key){
                    if (state['hide_' + key]) {
                        var el = root.querySelector('.fz-' + key);
                        if (el) el.style.display = 'none';
                    }
                });
            }
            applyItemVisibility();

            if (state.minimized) {
                root.classList.add('is-minimized');
            }

            root.querySelectorAll('.fz-close').forEach(function(btn){
                btn.addEventListener('click', function(e){
                    e.preventDefault();
                    var item = btn.closest('.fz-item');
                    if (item) {
                        item.style.display = 'none';

                        if (item.classList.contains('fz-wa'))    state.hide_wa    = true;
                        if (item.classList.contains('fz-ig'))    state.hide_ig    = true;
                        if (item.classList.contains('fz-ready')) state.hide_ready = true;

                        saveState(state);
                    }
                });
            });

            var groupBtn = root.querySelector('.fz-group-btn');
            if (groupBtn) {
                groupBtn.addEventListener('click', function(e){
                    e.preventDefault();
                    root.classList.add('is-minimized');
                    state.minimized = true;
                    saveState(state);
                });
            }

            var miniOpen = root.querySelector('.fz-mini-open');
            if (miniOpen) {
                miniOpen.addEventListener('click', function(e){
                    e.preventDefault();
                    root.classList.remove('is-minimized');
                    state.minimized = false;
                    saveState(state);
                });
            }

            var miniClose = root.querySelector('.fz-mini-close');
            if (miniClose) {
                miniClose.addEventListener('click', function(e){
                    e.preventDefault();
                    if (root && root.parentNode) {
                        root.parentNode.removeChild(root);
                    }
                    state.hidden = true;
                    saveState(state);
                });
            }
        })();
    </script>
    <?php
}
واتساپ
TEXT - 2026-06-07 22:05:51
// بنر واتساپ / اینستاگرام / محصولات آماده تحویل – با نگه‌داشتن وضعیت به مدت ۲۴ ساعت add_action( 'wp_footer', 'fz_quick_contact_banner' ); function fz_quick_contact_banner() { if ( function_exists('is_cart') && is_cart() ) return; if ( function_exists('is_checkout') && is_checkout() ) return; ?> <style> #fz-quick-contact { position: fixed; left: 8px; bottom: 16px; z-index: 9999; font-size: 12px; } #fz-quick-contact .fz-stack { position: relative; display: flex; flex-direction: column; gap: 6px; } /* دکمه جمع کردن بالای استک */ #fz-quick-contact .fz-group-btn { position: absolute; top: -24px; left: 0; background: #fff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 0 8px; height: 22px; font-size: 11px; display: flex; align-items: center; gap: 4px; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.08); } #fz-quick-contact .fz-group-btn span { font-size: 13px; } /* آیتم‌ها: چپ به راست → لینک → ضربدر */ #fz-quick-contact .fz-item { display: flex; align-items: center; direction: ltr; column-gap: 4px; /* ضربدر نزدیک لینک */ } /* لینک‌ها (واتساپ / اینستا / کارت) */ #fz-quick-contact .fz-link { flex: 0; /* واتساپ/اینستا کل عرض را نمی‌گیرند */ text-decoration: none; color: inherit; display: inline-flex; /* اندازه فقط به اندازه محتوا */ } /* فقط برای کارت آماده‌تحویل، لینک تمام‌عرض باشد */ #fz-quick-contact .fz-card .fz-link { flex: 1; display: flex; } #fz-quick-contact .fz-inner { display: flex; flex-direction: row; align-items: center; gap: 8px; } #fz-quick-contact .fz-icon { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } #fz-quick-contact .fz-icon svg { width: 16px; height: 16px; } #fz-quick-contact .fz-icon-wa { background:#25d366; } #fz-quick-contact .fz-icon-ig { background:#c13584; } #fz-quick-contact .fz-icon-ready { background:#e53935; } #fz-quick-contact .fz-text { direction: rtl; text-align: right; line-height: 1.4; white-space: nowrap; } #fz-quick-contact .fz-title { font-weight: 700; font-size: 13px; } #fz-quick-contact .fz-sub { font-size: 11px; color: #777; } #fz-quick-contact .fz-wa .fz-title { color:#1e9f4d; } #fz-quick-contact .fz-ig .fz-title { color:#c13584; } #fz-quick-contact .fz-ready .fz-title { color:#e53935; text-align:center; } #fz-quick-contact .fz-ready .fz-sub { text-align:center; } /* ضربدرها */ #fz-quick-contact .fz-close { background: transparent; border: none; font-size: 16px; line-height: 1; cursor: pointer; padding: 0; color: #666; margin-left: 0; } /* واتساپ / اینستاگرام: فقط قسمت لوگو+نوشته سفید و کوچک */ #fz-quick-contact .fz-line .fz-link { background: #ffffff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 4px 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.06); max-width: 260px; } /* محصولات آماده تحویل: کارت سفید بزرگ‌تر */ #fz-quick-contact .fz-card { padding: 6px 10px; border-radius: 8px; border: 1px solid #e3e3e3; box-shadow: 0 4px 10px rgba(0,0,0,0.05); background: #ffffff; } /* حالت جمع‌شده (مینی) */ #fz-quick-contact .fz-mini { display: none; align-items: center; gap: 6px; background: #ffffff; border-radius: 999px; border: 1px solid #e0e0e0; padding: 4px 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.08); margin-top: 4px; } #fz-quick-contact .fz-mini-icon { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; text-decoration: none; } #fz-quick-contact .fz-mini-icon svg { width: 16px; height: 16px; } #fz-quick-contact .fz-mini-wa { background:#25d366; } #fz-quick-contact .fz-mini-ig { background:#c13584; } #fz-quick-contact .fz-mini-ready { background:#e53935; } #fz-quick-contact .fz-mini-btn { border: none; background: transparent; cursor: pointer; font-size: 16px; line-height: 1; padding: 0 4px; color: #555; } #fz-quick-contact.is-minimized .fz-stack { display: none; } #fz-quick-contact.is-minimized .fz-group-btn { display: none; } #fz-quick-contact.is-minimized .fz-mini { display: flex; } @media (max-width:480px){ #fz-quick-contact { font-size: 11px; bottom: 14px; } } </style> <div id="fz-quick-contact"> <div class="fz-stack"> <!-- دکمه جمع کردن --> <button type="button" class="fz-group-btn"> <span>×</span> <small>جمع کردن</small> </button> <!-- واتساپ --> <div class="fz-item fz-line fz-wa"> <a class="fz-link" href="https://wa.me/989016161821"> <div class="fz-inner"> <span class="fz-icon fz-icon-wa"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/> <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/> </svg> </span> <span class="fz-text"> <span class="fz-title">واتساپ</span><br> <span class="fz-sub">چت سریع</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> <!-- اینستاگرام --> <div class="fz-item fz-line fz-ig"> <a class="fz-link" href="https://www.instagram.com/faryazandecor/"> <div class="fz-inner"> <span class="fz-icon fz-icon-ig"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/> <circle cx="12" cy="12" r="4" fill="#c13584"/> <circle cx="17" cy="7" r="1" fill="#c13584"/> </svg> </span> <span class="fz-text"> <span class="fz-title">اینستاگرام</span><br> <span class="fz-sub">پیج فریازان</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> <!-- محصولات آماده تحویل --> <div class="fz-item fz-card fz-ready"> <a class="fz-link" href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/"> <div class="fz-inner"> <span class="fz-icon fz-icon-ready"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="2" y="9" width="11" height="6" fill="#fff"/> <rect x="13" y="10" width="6" height="5" fill="#fff"/> <circle cx="7" cy="17" r="2" fill="#fff"/> <circle cx="16" cy="17" r="2" fill="#fff"/> </svg> </span> <span class="fz-text" style="text-align:center;"> <span class="fz-title">کلیک کنید</span><br> <span class="fz-sub">برای دیدن محصولات آماده تحویل</span> </span> </div> </a> <button type="button" class="fz-close" aria-label="بستن">×</button> </div> </div> <!-- حالت جمع‌شده --> <div class="fz-mini"> <button type="button" class="fz-mini-btn fz-mini-open" aria-label="باز کردن">‹</button> <a href="https://wa.me/989016161821" class="fz-mini-icon fz-mini-wa"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/> <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/> </svg> </a> <a href="https://www.instagram.com/faryazandecor/" class="fz-mini-icon fz-mini-ig"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/> <circle cx="12" cy="12" r="4" fill="#c13584"/> <circle cx="17" cy="7" r="1" fill="#c13584"/> </svg> </a> <a href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%AD%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/" class="fz-mini-icon fz-mini-ready"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <rect x="2" y="9" width="11" height="6" fill="#fff"/> <rect x="13" y="10" width="6" height="5" fill="#fff"/> <circle cx="7" cy="17" r="2" fill="#fff"/> <circle cx="16" cy="17" r="2" fill="#fff"/> </svg> </a> <button type="button" class="fz-mini-btn fz-mini-close" aria-label="بستن">×</button> </div> </div> <script> (function () { var root = document.getElementById('fz-quick-contact'); if (!root) return; var STORAGE_KEY = 'fzQuickContactState_v1'; var DAY_MS = 24 * 60 * 60 * 1000; function loadState() { try { var raw = localStorage.getItem(STORAGE_KEY); if (!raw) return {}; var obj = JSON.parse(raw); if (!obj.ts || Date.now() - obj.ts > DAY_MS) { localStorage.removeItem(STORAGE_KEY); return {}; } return obj; } catch (e) { return {}; } } function saveState(state) { state.ts = Date.now(); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} } var state = loadState(); // اگر کاربر کل بنر را بسته، کلاً نشان نده if (state.hidden) { if (root.parentNode) { root.parentNode.removeChild(root); } return; } // اعمال وضعیتِ آیتم‌های بسته و حالت جمع‌شده function applyItemVisibility() { ['wa', 'ig', 'ready'].forEach(function(key){ if (state['hide_' + key]) { var el = root.querySelector('.fz-' + key); if (el) el.style.display = 'none'; } }); } applyItemVisibility(); if (state.minimized) { root.classList.add('is-minimized'); } // بستن هر آیتم (واتساپ/اینستا/آماده تحویل) root.querySelectorAll('.fz-close').forEach(function(btn){ btn.addEventListener('click', function(e){ e.preventDefault(); var item = btn.closest('.fz-item'); if (item) { item.style.display = 'none'; if (item.classList.contains('fz-wa')) state.hide_wa = true; if (item.classList.contains('fz-ig')) state.hide_ig = true; if (item.classList.contains('fz-ready')) state.hide_ready = true; saveState(state); } }); }); // جمع کردن کل استک var groupBtn = root.querySelector('.fz-group-btn'); if (groupBtn) { groupBtn.addEventListener('click', function(e){ e.preventDefault(); root.classList.add('is-minimized'); state.minimized = true; saveState(state); }); } // باز کردن از حالت مینی var miniOpen = root.querySelector('.fz-mini-open'); if (miniOpen) { miniOpen.addEventListener('click', function(e){ e.preventDefault(); root.classList.remove('is-minimized'); state.minimized = false; saveState(state); }); } // حذف کامل در حالت مینی var miniClose = root.querySelector('.fz-mini-close'); if (miniClose) { miniClose.addEventListener('click', function(e){ e.preventDefault(); if (root && root.parentNode) { root.parentNode.removeChild(root); } state.hidden = true; saveState(state); }); } })(); </script> <?php }
// بنر واتساپ / اینستاگرام / محصولات آماده تحویل – با نگه‌داشتن وضعیت به مدت ۲۴ ساعت
add_action( 'wp_footer', 'fz_quick_contact_banner' );

function fz_quick_contact_banner() {

    if ( function_exists('is_cart') && is_cart() ) return;
    if ( function_exists('is_checkout') && is_checkout() ) return;
    ?>
    <style>
        #fz-quick-contact {
            position: fixed;
            left: 8px;
            bottom: 16px;
            z-index: 9999;
            font-size: 12px;
        }
        #fz-quick-contact .fz-stack {
            position: relative;
            display: flex;
            flex-direction: column;
            gap: 6px;
        }

        /* دکمه جمع کردن بالای استک */
        #fz-quick-contact .fz-group-btn {
            position: absolute;
            top: -24px;
            left: 0;
            background: #fff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 0 8px;
            height: 22px;
            font-size: 11px;
            display: flex;
            align-items: center;
            gap: 4px;
            cursor: pointer;
            box-shadow: 0 2px 6px rgba(0,0,0,0.08);
        }
        #fz-quick-contact .fz-group-btn span {
            font-size: 13px;
        }

        /* آیتم‌ها: چپ به راست → لینک → ضربدر */
        #fz-quick-contact .fz-item {
            display: flex;
            align-items: center;
            direction: ltr;
            column-gap: 4px; /* ضربدر نزدیک لینک */
        }

        /* لینک‌ها (واتساپ / اینستا / کارت) */
        #fz-quick-contact .fz-link {
            flex: 0;                     /* واتساپ/اینستا کل عرض را نمی‌گیرند */
            text-decoration: none;
            color: inherit;
            display: inline-flex;        /* اندازه فقط به اندازه محتوا */
        }

        /* فقط برای کارت آماده‌تحویل، لینک تمام‌عرض باشد */
        #fz-quick-contact .fz-card .fz-link {
            flex: 1;
            display: flex;
        }

        #fz-quick-contact .fz-inner {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 8px;
        }

        #fz-quick-contact .fz-icon {
            width: 24px;
            height: 24px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
        }
        #fz-quick-contact .fz-icon svg {
            width: 16px;
            height: 16px;
        }
        #fz-quick-contact .fz-icon-wa { background:#25d366; }
        #fz-quick-contact .fz-icon-ig { background:#c13584; }
        #fz-quick-contact .fz-icon-ready { background:#e53935; }

        #fz-quick-contact .fz-text {
            direction: rtl;
            text-align: right;
            line-height: 1.4;
            white-space: nowrap;
        }
        #fz-quick-contact .fz-title {
            font-weight: 700;
            font-size: 13px;
        }
        #fz-quick-contact .fz-sub {
            font-size: 11px;
            color: #777;
        }
        #fz-quick-contact .fz-wa .fz-title { color:#1e9f4d; }
        #fz-quick-contact .fz-ig .fz-title { color:#c13584; }
        #fz-quick-contact .fz-ready .fz-title { color:#e53935; text-align:center; }
        #fz-quick-contact .fz-ready .fz-sub { text-align:center; }

        /* ضربدرها */
        #fz-quick-contact .fz-close {
            background: transparent;
            border: none;
            font-size: 16px;
            line-height: 1;
            cursor: pointer;
            padding: 0;
            color: #666;
            margin-left: 0;
        }

        /* واتساپ / اینستاگرام: فقط قسمت لوگو+نوشته سفید و کوچک */
        #fz-quick-contact .fz-line .fz-link {
            background: #ffffff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 4px 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.06);
            max-width: 260px;
        }

        /* محصولات آماده تحویل: کارت سفید بزرگ‌تر */
        #fz-quick-contact .fz-card {
            padding: 6px 10px;
            border-radius: 8px;
            border: 1px solid #e3e3e3;
            box-shadow: 0 4px 10px rgba(0,0,0,0.05);
            background: #ffffff;
        }

        /* حالت جمع‌شده (مینی) */
        #fz-quick-contact .fz-mini {
            display: none;
            align-items: center;
            gap: 6px;
            background: #ffffff;
            border-radius: 999px;
            border: 1px solid #e0e0e0;
            padding: 4px 6px;
            box-shadow: 0 2px 6px rgba(0,0,0,0.08);
            margin-top: 4px;
        }
        #fz-quick-contact .fz-mini-icon {
            width: 26px;
            height: 26px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            text-decoration: none;
        }
        #fz-quick-contact .fz-mini-icon svg {
            width: 16px;
            height: 16px;
        }
        #fz-quick-contact .fz-mini-wa { background:#25d366; }
        #fz-quick-contact .fz-mini-ig { background:#c13584; }
        #fz-quick-contact .fz-mini-ready { background:#e53935; }

        #fz-quick-contact .fz-mini-btn {
            border: none;
            background: transparent;
            cursor: pointer;
            font-size: 16px;
            line-height: 1;
            padding: 0 4px;
            color: #555;
        }

        #fz-quick-contact.is-minimized .fz-stack {
            display: none;
        }
        #fz-quick-contact.is-minimized .fz-group-btn {
            display: none;
        }
        #fz-quick-contact.is-minimized .fz-mini {
            display: flex;
        }

        @media (max-width:480px){
            #fz-quick-contact {
                font-size: 11px;
                bottom: 14px;
            }
        }
    </style>

    <div id="fz-quick-contact">
        <div class="fz-stack">
            <!-- دکمه جمع کردن -->
            <button type="button" class="fz-group-btn">
                <span>×</span>
                <small>جمع کردن</small>
            </button>

            <!-- واتساپ -->
            <div class="fz-item fz-line fz-wa">
                <a class="fz-link" href="https://wa.me/989016161821">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-wa">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/>
                                <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/>
                            </svg>
                        </span>
                        <span class="fz-text">
                            <span class="fz-title">واتساپ</span><br>
                            <span class="fz-sub">چت سریع</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>

            <!-- اینستاگرام -->
            <div class="fz-item fz-line fz-ig">
                <a class="fz-link" href="https://www.instagram.com/faryazandecor/">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-ig">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/>
                                <circle cx="12" cy="12" r="4" fill="#c13584"/>
                                <circle cx="17" cy="7" r="1" fill="#c13584"/>
                            </svg>
                        </span>
                        <span class="fz-text">
                            <span class="fz-title">اینستاگرام</span><br>
                            <span class="fz-sub">پیج فریازان</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>

            <!-- محصولات آماده تحویل -->
            <div class="fz-item fz-card fz-ready">
                <a class="fz-link" href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%B5%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/">
                    <div class="fz-inner">
                        <span class="fz-icon fz-icon-ready">
                            <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <rect x="2" y="9" width="11" height="6" fill="#fff"/>
                                <rect x="13" y="10" width="6" height="5" fill="#fff"/>
                                <circle cx="7" cy="17" r="2" fill="#fff"/>
                                <circle cx="16" cy="17" r="2" fill="#fff"/>
                            </svg>
                        </span>
                        <span class="fz-text" style="text-align:center;">
                            <span class="fz-title">کلیک کنید</span><br>
                            <span class="fz-sub">برای دیدن محصولات آماده تحویل</span>
                        </span>
                    </div>
                </a>
                <button type="button" class="fz-close" aria-label="بستن">×</button>
            </div>
        </div>

        <!-- حالت جمع‌شده -->
        <div class="fz-mini">
            <button type="button" class="fz-mini-btn fz-mini-open" aria-label="باز کردن">‹</button>

            <a href="https://wa.me/989016161821" class="fz-mini-icon fz-mini-wa">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path d="M12 3a8 8 0 0 0-6.9 12.1L4 21l6-1.1A8 8 0 1 0 12 3z" fill="#fff"/>
                    <path d="M10.5 8.5c-.2-.5-.3-.5-.6-.5h-.5c-.2 0-.5.1-.7.3-.2.2-.9.9-.9 2.1 0 1.2.9 2.3 1 2.4.1.2 1.8 2.8 4.3 3.8 2.1.8 2.5.7 3 .7.5 0 1.5-.6 1.7-1.3.2-.6.2-1.1.2-1.2-.1-.1-.2-.2-.5-.4s-1.5-.7-1.7-.8c-.2-.1-.4-.1-.6.1l-.4.6c-.1.2-.3.3-.5.2-.2-.1-1-.4-1.9-1.3-.7-.6-1.2-1.4-1.3-1.6-.1-.2 0-.3.1-.5l.3-.3c.1-.1.2-.3.2-.5 0-.2-.6-1.6-.8-1.9z" fill="#25d366"/>
                </svg>
            </a>
            <a href="https://www.instagram.com/faryazandecor/" class="fz-mini-icon fz-mini-ig">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <rect x="5" y="5" width="14" height="14" rx="4" ry="4" fill="#fff"/>
                    <circle cx="12" cy="12" r="4" fill="#c13584"/>
                    <circle cx="17" cy="7" r="1" fill="#c13584"/>
                </svg>
            </a>
            <a href="https://faryazandecor.com/product-category/%D9%85%D8%AD%D8%AD%D9%88%D9%84%D8%A7%D8%AA-%D8%A2%D9%85%D8%A7%D8%AF%D9%87-%D8%AA%D8%AD%D9%88%DB%8C%D9%84/" class="fz-mini-icon fz-mini-ready">
                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <rect x="2" y="9" width="11" height="6" fill="#fff"/>
                    <rect x="13" y="10" width="6" height="5" fill="#fff"/>
                    <circle cx="7" cy="17" r="2" fill="#fff"/>
                    <circle cx="16" cy="17" r="2" fill="#fff"/>
                </svg>
            </a>

            <button type="button" class="fz-mini-btn fz-mini-close" aria-label="بستن">×</button>
        </div>
    </div>

    <script>
        (function () {
            var root = document.getElementById('fz-quick-contact');
            if (!root) return;

            var STORAGE_KEY = 'fzQuickContactState_v1';
            var DAY_MS = 24 * 60 * 60 * 1000;

            function loadState() {
                try {
                    var raw = localStorage.getItem(STORAGE_KEY);
                    if (!raw) return {};
                    var obj = JSON.parse(raw);
                    if (!obj.ts || Date.now() - obj.ts > DAY_MS) {
                        localStorage.removeItem(STORAGE_KEY);
                        return {};
                    }
                    return obj;
                } catch (e) {
                    return {};
                }
            }

            function saveState(state) {
                state.ts = Date.now();
                try {
                    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
                } catch (e) {}
            }

            var state = loadState();

            // اگر کاربر کل بنر را بسته، کلاً نشان نده
            if (state.hidden) {
                if (root.parentNode) {
                    root.parentNode.removeChild(root);
                }
                return;
            }

            // اعمال وضعیتِ آیتم‌های بسته و حالت جمع‌شده
            function applyItemVisibility() {
                ['wa', 'ig', 'ready'].forEach(function(key){
                    if (state['hide_' + key]) {
                        var el = root.querySelector('.fz-' + key);
                        if (el) el.style.display = 'none';
                    }
                });
            }
            applyItemVisibility();

            if (state.minimized) {
                root.classList.add('is-minimized');
            }

            // بستن هر آیتم (واتساپ/اینستا/آماده تحویل)
            root.querySelectorAll('.fz-close').forEach(function(btn){
                btn.addEventListener('click', function(e){
                    e.preventDefault();
                    var item = btn.closest('.fz-item');
                    if (item) {
                        item.style.display = 'none';

                        if (item.classList.contains('fz-wa'))    state.hide_wa    = true;
                        if (item.classList.contains('fz-ig'))    state.hide_ig    = true;
                        if (item.classList.contains('fz-ready')) state.hide_ready = true;

                        saveState(state);
                    }
                });
            });

            // جمع کردن کل استک
            var groupBtn = root.querySelector('.fz-group-btn');
            if (groupBtn) {
                groupBtn.addEventListener('click', function(e){
                    e.preventDefault();
                    root.classList.add('is-minimized');
                    state.minimized = true;
                    saveState(state);
                });
            }

            // باز کردن از حالت مینی
            var miniOpen = root.querySelector('.fz-mini-open');
            if (miniOpen) {
                miniOpen.addEventListener('click', function(e){
                    e.preventDefault();
                    root.classList.remove('is-minimized');
                    state.minimized = false;
                    saveState(state);
                });
            }

            // حذف کامل در حالت مینی
            var miniClose = root.querySelector('.fz-mini-close');
            if (miniClose) {
                miniClose.addEventListener('click', function(e){
                    e.preventDefault();
                    if (root && root.parentNode) {
                        root.parentNode.removeChild(root);
                    }
                    state.hidden = true;
                    saveState(state);
                });
            }
        })();
    </script>
    <?php
}
تنوع از صفحه محصول
TEXT - 2026-06-05 21:54:52
if (!defined('ABSPATH')) exit; /** * فقط ادمین */ function qv_is_admin_user() { return current_user_can('manage_woocommerce') || current_user_can('administrator'); } /** * لیبل attribute */ function qv_get_attribute_label_safe($name, $product = null) { if (function_exists('wc_attribute_label')) { $label = wc_attribute_label($name, $product); if (!empty($label)) return $label; } if (strpos($name, 'pa_') === 0) { $name = str_replace('pa_', '', $name); } return ucfirst(str_replace(array('-', '_'), ' ', $name)); } /** * متن خوانا برای option */ function qv_get_readable_option_label($attribute_name, $option_value) { if ($option_value === '' || $option_value === null) { return ''; } if (taxonomy_exists($attribute_name)) { $term = get_term_by('slug', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } $term = get_term_by('name', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } } $decoded = rawurldecode($option_value); $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8'); return $decoded; } /** * همه attributeهای قابل انتخاب * - هم attributeهای روی خود محصول * - هم همه attributeهای سراسری ووکامرس */ function qv_get_all_selectable_attributes($product) { $result = array(); $map = array(); /** * 1) اول attributeهای خود محصول */ $product_attributes = $product->get_attributes(); if (!empty($product_attributes)) { foreach ($product_attributes as $attribute_key => $attribute_obj) { if (!is_a($attribute_obj, 'WC_Product_Attribute')) { continue; } $attribute_name = $attribute_obj->get_name(); $label = qv_get_attribute_label_safe($attribute_name, $product); $options = array(); if ($attribute_obj->is_taxonomy()) { $terms = wc_get_product_terms($product->get_id(), $attribute_name, array('fields' => 'all')); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => $term->slug, 'label' => $term->name, ); } } } else { $raw_options = $attribute_obj->get_options(); if (!empty($raw_options)) { foreach ($raw_options as $opt) { if ($opt === '' || $opt === null) continue; $options[] = array( 'value' => $opt, 'label' => qv_get_readable_option_label($attribute_name, $opt), ); } } } if (!isset($map[$attribute_name])) { $map[$attribute_name] = array( 'name' => $attribute_name, 'label' => $label, 'options' => array(), ); } foreach ($options as $opt) { $map[$attribute_name]['options'][(string)$opt['value']] = $opt; } } } /** * 2) همه attributeهای سراسری ووکامرس */ $global_attributes = function_exists('wc_get_attribute_taxonomies') ? wc_get_attribute_taxonomies() : array(); if (!empty($global_attributes)) { foreach ($global_attributes as $ga) { if (empty($ga->attribute_name)) continue; $taxonomy = wc_attribute_taxonomy_name($ga->attribute_name); if (!taxonomy_exists($taxonomy)) continue; $label = !empty($ga->attribute_label) ? $ga->attribute_label : qv_get_attribute_label_safe($taxonomy, $product); if (!isset($map[$taxonomy])) { $map[$taxonomy] = array( 'name' => $taxonomy, 'label' => $label, 'options' => array(), ); } $terms = get_terms(array( 'taxonomy' => $taxonomy, 'hide_empty' => false, )); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $map[$taxonomy]['options'][(string)$term->slug] = array( 'value' => $term->slug, 'label' => $term->name, ); } } } } foreach ($map as $attribute_name => $item) { if (!empty($item['options'])) { $item['options'] = array_values($item['options']); $result[] = $item; } } return $result; } /** * تبدیل محصول به variable */ function qv_ensure_variable_product($product_id) { $product = wc_get_product($product_id); if (!$product) return false; if ($product->is_type('variable')) { return true; } wp_set_object_terms($product_id, 'variable', 'product_type'); clean_post_cache($product_id); $product = wc_get_product($product_id); return ($product && $product->is_type('variable')); } /** * افزودن attribute به محصول اگر نبود */ function qv_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') { $product = wc_get_product($product_id); if (!$product) return false; $attributes = $product->get_attributes(); if (isset($attributes[$attribute_name])) { $attr_obj = $attributes[$attribute_name]; if (is_a($attr_obj, 'WC_Product_Attribute')) { $attr_obj->set_visible(true); $attr_obj->set_variation(true); if (!$attr_obj->is_taxonomy() && $attribute_value !== '') { $options = $attr_obj->get_options(); if (!in_array($attribute_value, $options, true)) { $options[] = $attribute_value; $attr_obj->set_options($options); } } $attributes[$attribute_name] = $attr_obj; $product->set_attributes($attributes); $product->save(); } return true; } $new_attr = new WC_Product_Attribute(); if (taxonomy_exists($attribute_name)) { $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0; $new_attr->set_id($taxonomy_id); $new_attr->set_name($attribute_name); $new_attr->set_options(array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } else { $new_attr->set_id(0); $new_attr->set_name($attribute_name); $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } $attributes[$attribute_name] = $new_attr; $product->set_attributes($attributes); $product->save(); return true; } /** * variation تکراری */ function qv_variation_exists($product_id, $variation_attributes) { $children = get_posts(array( 'post_parent' => $product_id, 'post_type' => 'product_variation', 'post_status' => array('publish', 'private'), 'numberposts' => -1, 'fields' => 'ids', )); if (empty($children)) return false; foreach ($children as $variation_id) { $same = true; foreach ($variation_attributes as $key => $value) { $existing = get_post_meta($variation_id, $key, true); if ((string)$existing !== (string)$value) { $same = false; break; } } if ($same) { return true; } } return false; } /** * فرم */ function qv_render_quick_variation_form() { if (!is_product()) return; if (!qv_is_admin_user()) return; global $product; if (!$product || !is_a($product, 'WC_Product')) return; $attributes = qv_get_all_selectable_attributes($product); if (empty($attributes)) return; ?> <div class="qv-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;"> <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3> <form method="post" class="qv-quick-variation-form" autocomplete="off" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <?php wp_nonce_field('qv_quick_variation_action', 'qv_quick_variation_nonce'); ?> <input type="hidden" name="qv_product_id" value="<?php echo esc_attr($product->get_id()); ?>"> <div> <label style="display:block;margin-bottom:6px;">ویژگی اول</label> <select name="qv_attr1" id="qv_attr1_custom" style="width:100%;padding:8px;"> <option value="">انتخاب ویژگی</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label> <select name="qv_val1" id="qv_val1_custom" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div> <label style="display:block;margin-bottom:6px;">ویژگی دوم</label> <select name="qv_attr2" id="qv_attr2_custom" style="width:100%;padding:8px;"> <option value="">بدون ویژگی دوم</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label> <select name="qv_val2" id="qv_val2_custom" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div style="grid-column:1/-1;"> <label style="display:block;margin-bottom:6px;">قیمت</label> <input type="number" step="0.01" min="0" name="qv_price" required style="width:100%;padding:8px;"> </div> <div style="grid-column:1/-1;"> <button type="submit" name="qv_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;"> افزودن تنوع </button> </div> </form> </div> <script> (function(){ var attributes = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>; var attr1 = document.getElementById('qv_attr1_custom'); var val1 = document.getElementById('qv_val1_custom'); var attr2 = document.getElementById('qv_attr2_custom'); var val2 = document.getElementById('qv_val2_custom'); if (!attr1 || !val1 || !attr2 || !val2) return; function findAttribute(name) { for (var i = 0; i < attributes.length; i++) { if (attributes[i].name === name) return attributes[i]; } return null; } function fillValues(attrSelect, valueSelect) { var attrName = attrSelect.value; var previousValue = valueSelect.value || ''; valueSelect.innerHTML = ''; if (!attrName) { var p = document.createElement('option'); p.value = ''; p.textContent = 'ابتدا ویژگی را انتخاب کنید'; valueSelect.appendChild(p); return; } var data = findAttribute(attrName); var first = document.createElement('option'); first.value = ''; first.textContent = 'انتخاب مقدار'; valueSelect.appendChild(first); var any = document.createElement('option'); any.value = '__any__'; any.textContent = 'همه موارد'; valueSelect.appendChild(any); if (data && data.options) { data.options.forEach(function(opt){ var option = document.createElement('option'); option.value = opt.value; option.textContent = opt.label; valueSelect.appendChild(option); }); } if (previousValue) { var exists = false; for (var i = 0; i < valueSelect.options.length; i++) { if (valueSelect.options[i].value === previousValue) { exists = true; break; } } valueSelect.value = exists ? previousValue : ''; } } attr1.addEventListener('change', function(e){ e.stopPropagation(); fillValues(attr1, val1); if (attr2.value && attr2.value === attr1.value) { attr2.value = ''; fillValues(attr2, val2); } }, true); attr2.addEventListener('change', function(e){ e.stopPropagation(); if (attr1.value && attr2.value && attr1.value === attr2.value) { alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.'); attr2.value = ''; } fillValues(attr2, val2); }, true); val1.addEventListener('change', function(e){ e.stopPropagation(); }, true); val2.addEventListener('change', function(e){ e.stopPropagation(); }, true); attr1.addEventListener('click', function(e){ e.stopPropagation(); }, true); attr2.addEventListener('click', function(e){ e.stopPropagation(); }, true); val1.addEventListener('click', function(e){ e.stopPropagation(); }, true); val2.addEventListener('click', function(e){ e.stopPropagation(); }, true); })(); </script> <?php } add_action('woocommerce_after_single_product_summary', 'qv_render_quick_variation_form', 5); /** * ثبت فرم */ function qv_handle_quick_variation_submit() { if (!isset($_POST['qv_quick_variation_submit'])) return; if (!qv_is_admin_user()) return; if (!isset($_POST['qv_quick_variation_nonce']) || !wp_verify_nonce($_POST['qv_quick_variation_nonce'], 'qv_quick_variation_action')) { return; } $product_id = isset($_POST['qv_product_id']) ? absint($_POST['qv_product_id']) : 0; $attr1 = isset($_POST['qv_attr1']) ? wc_clean(wp_unslash($_POST['qv_attr1'])) : ''; $val1 = isset($_POST['qv_val1']) ? wc_clean(wp_unslash($_POST['qv_val1'])) : ''; $attr2 = isset($_POST['qv_attr2']) ? wc_clean(wp_unslash($_POST['qv_attr2'])) : ''; $val2 = isset($_POST['qv_val2']) ? wc_clean(wp_unslash($_POST['qv_val2'])) : ''; $price = isset($_POST['qv_price']) ? wc_format_decimal(wp_unslash($_POST['qv_price'])) : ''; if (!$product_id || !$attr1 || $val1 === '' || $price === '') { wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error'); return; } if ($attr2 && !$val2 && $val2 !== '__any__') { wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error'); return; } if ($attr1 && $attr2 && $attr1 === $attr2) { wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error'); return; } if ($val1 === '__any__' && $val2 === '__any__') { wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان همه موارد را انتخاب کرد.', 'error'); return; } if (!qv_ensure_variable_product($product_id)) { wc_add_notice('تبدیل محصول به variable ناموفق بود.', 'error'); return; } qv_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : ''); if ($attr2) { qv_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : ''); } $variation_attributes = array( 'attribute_' . $attr1 => ($val1 === '__any__' ? '' : $val1), ); if ($attr2) { $variation_attributes['attribute_' . $attr2] = ($val2 === '__any__' ? '' : $val2); } if (qv_variation_exists($product_id, $variation_attributes)) { wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error'); return; } $variation_post = array( 'post_title' => 'Product variation', 'post_name' => 'product-' . $product_id . '-variation', 'post_status' => 'publish', 'post_parent' => $product_id, 'post_type' => 'product_variation', 'guid' => home_url('/?product_variation=product-' . $product_id . '-variation'), ); $variation_id = wp_insert_post($variation_post); if (!$variation_id || is_wp_error($variation_id)) { wc_add_notice('ساخت variation ناموفق بود.', 'error'); return; } foreach ($variation_attributes as $meta_key => $meta_value) { update_post_meta($variation_id, $meta_key, $meta_value); } update_post_meta($variation_id, '_regular_price', $price); update_post_meta($variation_id, '_price', $price); $variation = new WC_Product_Variation($variation_id); $variation->set_parent_id($product_id); $variation->set_regular_price($price); $variation->set_price($price); $set_attrs = array( $attr1 => ($val1 === '__any__' ? '' : $val1), ); if ($attr2) { $set_attrs[$attr2] = ($val2 === '__any__' ? '' : $val2); } $variation->set_attributes($set_attrs); $variation->save(); WC_Product_Variable::sync($product_id); wc_delete_product_transients($product_id); wc_add_notice('تنوع جدید با موفقیت ساخته شد.', 'success'); } add_action('init', 'qv_handle_quick_variation_submit');
if (!defined('ABSPATH')) exit;

/**
 * فقط ادمین
 */
function qv_is_admin_user() {
    return current_user_can('manage_woocommerce') || current_user_can('administrator');
}

/**
 * لیبل attribute
 */
function qv_get_attribute_label_safe($name, $product = null) {
    if (function_exists('wc_attribute_label')) {
        $label = wc_attribute_label($name, $product);
        if (!empty($label)) return $label;
    }

    if (strpos($name, 'pa_') === 0) {
        $name = str_replace('pa_', '', $name);
    }

    return ucfirst(str_replace(array('-', '_'), ' ', $name));
}

/**
 * متن خوانا برای option
 */
function qv_get_readable_option_label($attribute_name, $option_value) {
    if ($option_value === '' || $option_value === null) {
        return '';
    }

    if (taxonomy_exists($attribute_name)) {
        $term = get_term_by('slug', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }

        $term = get_term_by('name', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }
    }

    $decoded = rawurldecode($option_value);
    $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8');
    return $decoded;
}

/**
 * همه attributeهای قابل انتخاب
 * - هم attributeهای روی خود محصول
 * - هم همه attributeهای سراسری ووکامرس
 */
function qv_get_all_selectable_attributes($product) {
    $result = array();
    $map = array();

    /**
     * 1) اول attributeهای خود محصول
     */
    $product_attributes = $product->get_attributes();

    if (!empty($product_attributes)) {
        foreach ($product_attributes as $attribute_key => $attribute_obj) {
            if (!is_a($attribute_obj, 'WC_Product_Attribute')) {
                continue;
            }

            $attribute_name = $attribute_obj->get_name();
            $label = qv_get_attribute_label_safe($attribute_name, $product);
            $options = array();

            if ($attribute_obj->is_taxonomy()) {
                $terms = wc_get_product_terms($product->get_id(), $attribute_name, array('fields' => 'all'));

                if (!empty($terms) && !is_wp_error($terms)) {
                    foreach ($terms as $term) {
                        $options[] = array(
                            'value' => $term->slug,
                            'label' => $term->name,
                        );
                    }
                }
            } else {
                $raw_options = $attribute_obj->get_options();

                if (!empty($raw_options)) {
                    foreach ($raw_options as $opt) {
                        if ($opt === '' || $opt === null) continue;

                        $options[] = array(
                            'value' => $opt,
                            'label' => qv_get_readable_option_label($attribute_name, $opt),
                        );
                    }
                }
            }

            if (!isset($map[$attribute_name])) {
                $map[$attribute_name] = array(
                    'name'    => $attribute_name,
                    'label'   => $label,
                    'options' => array(),
                );
            }

            foreach ($options as $opt) {
                $map[$attribute_name]['options'][(string)$opt['value']] = $opt;
            }
        }
    }

    /**
     * 2) همه attributeهای سراسری ووکامرس
     */
    $global_attributes = function_exists('wc_get_attribute_taxonomies') ? wc_get_attribute_taxonomies() : array();

    if (!empty($global_attributes)) {
        foreach ($global_attributes as $ga) {
            if (empty($ga->attribute_name)) continue;

            $taxonomy = wc_attribute_taxonomy_name($ga->attribute_name);
            if (!taxonomy_exists($taxonomy)) continue;

            $label = !empty($ga->attribute_label) ? $ga->attribute_label : qv_get_attribute_label_safe($taxonomy, $product);

            if (!isset($map[$taxonomy])) {
                $map[$taxonomy] = array(
                    'name'    => $taxonomy,
                    'label'   => $label,
                    'options' => array(),
                );
            }

            $terms = get_terms(array(
                'taxonomy'   => $taxonomy,
                'hide_empty' => false,
            ));

            if (!empty($terms) && !is_wp_error($terms)) {
                foreach ($terms as $term) {
                    $map[$taxonomy]['options'][(string)$term->slug] = array(
                        'value' => $term->slug,
                        'label' => $term->name,
                    );
                }
            }
        }
    }

    foreach ($map as $attribute_name => $item) {
        if (!empty($item['options'])) {
            $item['options'] = array_values($item['options']);
            $result[] = $item;
        }
    }

    return $result;
}

/**
 * تبدیل محصول به variable
 */
function qv_ensure_variable_product($product_id) {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    if ($product->is_type('variable')) {
        return true;
    }

    wp_set_object_terms($product_id, 'variable', 'product_type');
    clean_post_cache($product_id);

    $product = wc_get_product($product_id);
    return ($product && $product->is_type('variable'));
}

/**
 * افزودن attribute به محصول اگر نبود
 */
function qv_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    $attributes = $product->get_attributes();

    if (isset($attributes[$attribute_name])) {
        $attr_obj = $attributes[$attribute_name];

        if (is_a($attr_obj, 'WC_Product_Attribute')) {
            $attr_obj->set_visible(true);
            $attr_obj->set_variation(true);

            if (!$attr_obj->is_taxonomy() && $attribute_value !== '') {
                $options = $attr_obj->get_options();
                if (!in_array($attribute_value, $options, true)) {
                    $options[] = $attribute_value;
                    $attr_obj->set_options($options);
                }
            }

            $attributes[$attribute_name] = $attr_obj;
            $product->set_attributes($attributes);
            $product->save();
        }

        return true;
    }

    $new_attr = new WC_Product_Attribute();

    if (taxonomy_exists($attribute_name)) {
        $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0;
        $new_attr->set_id($taxonomy_id);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options(array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    } else {
        $new_attr->set_id(0);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    }

    $attributes[$attribute_name] = $new_attr;
    $product->set_attributes($attributes);
    $product->save();

    return true;
}

/**
 * variation تکراری
 */
function qv_variation_exists($product_id, $variation_attributes) {
    $children = get_posts(array(
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'post_status' => array('publish', 'private'),
        'numberposts' => -1,
        'fields'      => 'ids',
    ));

    if (empty($children)) return false;

    foreach ($children as $variation_id) {
        $same = true;

        foreach ($variation_attributes as $key => $value) {
            $existing = get_post_meta($variation_id, $key, true);
            if ((string)$existing !== (string)$value) {
                $same = false;
                break;
            }
        }

        if ($same) {
            return true;
        }
    }

    return false;
}

/**
 * فرم
 */
function qv_render_quick_variation_form() {
    if (!is_product()) return;
    if (!qv_is_admin_user()) return;

    global $product;
    if (!$product || !is_a($product, 'WC_Product')) return;

    $attributes = qv_get_all_selectable_attributes($product);
    if (empty($attributes)) return;
    ?>
    <div class="qv-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;">
        <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3>

        <form method="post" class="qv-quick-variation-form" autocomplete="off" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
            <?php wp_nonce_field('qv_quick_variation_action', 'qv_quick_variation_nonce'); ?>
            <input type="hidden" name="qv_product_id" value="<?php echo esc_attr($product->get_id()); ?>">

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی اول</label>
                <select name="qv_attr1" id="qv_attr1_custom" style="width:100%;padding:8px;">
                    <option value="">انتخاب ویژگی</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label>
                <select name="qv_val1" id="qv_val1_custom" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی دوم</label>
                <select name="qv_attr2" id="qv_attr2_custom" style="width:100%;padding:8px;">
                    <option value="">بدون ویژگی دوم</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label>
                <select name="qv_val2" id="qv_val2_custom" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div style="grid-column:1/-1;">
                <label style="display:block;margin-bottom:6px;">قیمت</label>
                <input type="number" step="0.01" min="0" name="qv_price" required style="width:100%;padding:8px;">
            </div>

            <div style="grid-column:1/-1;">
                <button type="submit" name="qv_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;">
                    افزودن تنوع
                </button>
            </div>
        </form>
    </div>

    <script>
    (function(){
        var attributes = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;

        var attr1 = document.getElementById('qv_attr1_custom');
        var val1  = document.getElementById('qv_val1_custom');
        var attr2 = document.getElementById('qv_attr2_custom');
        var val2  = document.getElementById('qv_val2_custom');

        if (!attr1 || !val1 || !attr2 || !val2) return;

        function findAttribute(name) {
            for (var i = 0; i < attributes.length; i++) {
                if (attributes[i].name === name) return attributes[i];
            }
            return null;
        }

        function fillValues(attrSelect, valueSelect) {
            var attrName = attrSelect.value;
            var previousValue = valueSelect.value || '';

            valueSelect.innerHTML = '';

            if (!attrName) {
                var p = document.createElement('option');
                p.value = '';
                p.textContent = 'ابتدا ویژگی را انتخاب کنید';
                valueSelect.appendChild(p);
                return;
            }

            var data = findAttribute(attrName);

            var first = document.createElement('option');
            first.value = '';
            first.textContent = 'انتخاب مقدار';
            valueSelect.appendChild(first);

            var any = document.createElement('option');
            any.value = '__any__';
            any.textContent = 'همه موارد';
            valueSelect.appendChild(any);

            if (data && data.options) {
                data.options.forEach(function(opt){
                    var option = document.createElement('option');
                    option.value = opt.value;
                    option.textContent = opt.label;
                    valueSelect.appendChild(option);
                });
            }

            if (previousValue) {
                var exists = false;
                for (var i = 0; i < valueSelect.options.length; i++) {
                    if (valueSelect.options[i].value === previousValue) {
                        exists = true;
                        break;
                    }
                }
                valueSelect.value = exists ? previousValue : '';
            }
        }

        attr1.addEventListener('change', function(e){
            e.stopPropagation();
            fillValues(attr1, val1);

            if (attr2.value && attr2.value === attr1.value) {
                attr2.value = '';
                fillValues(attr2, val2);
            }
        }, true);

        attr2.addEventListener('change', function(e){
            e.stopPropagation();

            if (attr1.value && attr2.value && attr1.value === attr2.value) {
                alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.');
                attr2.value = '';
            }

            fillValues(attr2, val2);
        }, true);

        val1.addEventListener('change', function(e){
            e.stopPropagation();
        }, true);

        val2.addEventListener('change', function(e){
            e.stopPropagation();
        }, true);

        attr1.addEventListener('click', function(e){ e.stopPropagation(); }, true);
        attr2.addEventListener('click', function(e){ e.stopPropagation(); }, true);
        val1.addEventListener('click', function(e){ e.stopPropagation(); }, true);
        val2.addEventListener('click', function(e){ e.stopPropagation(); }, true);
    })();
    </script>
    <?php
}
add_action('woocommerce_after_single_product_summary', 'qv_render_quick_variation_form', 5);

/**
 * ثبت فرم
 */
function qv_handle_quick_variation_submit() {
    if (!isset($_POST['qv_quick_variation_submit'])) return;
    if (!qv_is_admin_user()) return;

    if (!isset($_POST['qv_quick_variation_nonce']) || !wp_verify_nonce($_POST['qv_quick_variation_nonce'], 'qv_quick_variation_action')) {
        return;
    }

    $product_id = isset($_POST['qv_product_id']) ? absint($_POST['qv_product_id']) : 0;
    $attr1      = isset($_POST['qv_attr1']) ? wc_clean(wp_unslash($_POST['qv_attr1'])) : '';
    $val1       = isset($_POST['qv_val1']) ? wc_clean(wp_unslash($_POST['qv_val1'])) : '';
    $attr2      = isset($_POST['qv_attr2']) ? wc_clean(wp_unslash($_POST['qv_attr2'])) : '';
    $val2       = isset($_POST['qv_val2']) ? wc_clean(wp_unslash($_POST['qv_val2'])) : '';
    $price      = isset($_POST['qv_price']) ? wc_format_decimal(wp_unslash($_POST['qv_price'])) : '';

    if (!$product_id || !$attr1 || $val1 === '' || $price === '') {
        wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error');
        return;
    }

    if ($attr2 && !$val2 && $val2 !== '__any__') {
        wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error');
        return;
    }

    if ($attr1 && $attr2 && $attr1 === $attr2) {
        wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error');
        return;
    }

    if ($val1 === '__any__' && $val2 === '__any__') {
        wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان همه موارد را انتخاب کرد.', 'error');
        return;
    }

    if (!qv_ensure_variable_product($product_id)) {
        wc_add_notice('تبدیل محصول به variable ناموفق بود.', 'error');
        return;
    }

    qv_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : '');
    if ($attr2) {
        qv_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : '');
    }

    $variation_attributes = array(
        'attribute_' . $attr1 => ($val1 === '__any__' ? '' : $val1),
    );

    if ($attr2) {
        $variation_attributes['attribute_' . $attr2] = ($val2 === '__any__' ? '' : $val2);
    }

    if (qv_variation_exists($product_id, $variation_attributes)) {
        wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error');
        return;
    }

    $variation_post = array(
        'post_title'  => 'Product variation',
        'post_name'   => 'product-' . $product_id . '-variation',
        'post_status' => 'publish',
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'guid'        => home_url('/?product_variation=product-' . $product_id . '-variation'),
    );

    $variation_id = wp_insert_post($variation_post);

    if (!$variation_id || is_wp_error($variation_id)) {
        wc_add_notice('ساخت variation ناموفق بود.', 'error');
        return;
    }

    foreach ($variation_attributes as $meta_key => $meta_value) {
        update_post_meta($variation_id, $meta_key, $meta_value);
    }

    update_post_meta($variation_id, '_regular_price', $price);
    update_post_meta($variation_id, '_price', $price);

    $variation = new WC_Product_Variation($variation_id);
    $variation->set_parent_id($product_id);
    $variation->set_regular_price($price);
    $variation->set_price($price);

    $set_attrs = array(
        $attr1 => ($val1 === '__any__' ? '' : $val1),
    );

    if ($attr2) {
        $set_attrs[$attr2] = ($val2 === '__any__' ? '' : $val2);
    }

    $variation->set_attributes($set_attrs);
    $variation->save();

    WC_Product_Variable::sync($product_id);
    wc_delete_product_transients($product_id);

    wc_add_notice('تنوع جدید با موفقیت ساخته شد.', 'success');
}
add_action('init', 'qv_handle_quick_variation_submit');
محصول جدید
TEXT - 2026-06-05 21:17:33
<?php /** * Plugin Name: Faryazan Simple Product Uploader * Description: Mobile-first uploader for WooCommerce. REST + AJAX fallback. Upload progress 0-100. Price display with thousands separators (visual only). SEO meta for images + gallery images appended to description. Title appended as H2 in description. * Version: 1.1.0 * Author: Faryazan */ if (!defined('ABSPATH')) exit; add_action('init', function () { add_shortcode('faryazan_simple_uploader', 'fzspu_shortcode'); }); function fzspu_can_use() { return is_user_logged_in() && (current_user_can('manage_woocommerce') || current_user_can('administrator')); } add_action('wp_enqueue_scripts', function () { if (!is_singular()) return; global $post; if (!$post || (!has_shortcode($post->post_content, 'faryazan_simple_uploader') && !has_shortcode($post->post_content, 'fzspu'))) return; if (!fzspu_can_use()) return; $ver = '1.0.8'; wp_register_style('fzspu_css', false, [], $ver); wp_enqueue_style('fzspu_css'); wp_add_inline_style('fzspu_css', fzspu_css()); // Needed for WordPress Media Library picker on front-end if (function_exists('wp_enqueue_media')) { wp_enqueue_media(); } wp_register_script('fzspu_js', false, ['jquery'], $ver, true); wp_enqueue_script('fzspu_js'); wp_add_inline_script('fzspu_js', 'window.FZSPU = ' . wp_json_encode([ 'restNonce' => wp_create_nonce('wp_rest'), 'ajaxNonce' => wp_create_nonce('fzspu_ajax'), 'ajaxUrl' => admin_url('admin-ajax.php'), ]) . ';', 'before'); wp_add_inline_script('fzspu_js', fzspu_js(), 'after'); }); function fzspu_shortcode() { if (!fzspu_can_use()) { return '<div style="padding:16px;font-family:tahoma">برای استفاده باید با اکانت مدیر وارد شوید.</div>'; } ob_start(); ?> <div class="fzspu-app" dir="rtl"> <div class="fzspu-topbar"> <div class="fzspu-iconbtn" data-action="close">×</div> <div class="fzspu-title">ثبت آگهی</div> <div class="fzspu-iconbtn" id="fzspuBack" style="visibility:hidden;">→</div> </div> <div class="fzspu-wrap"> <div class="fzspu-screen active" id="fzspuS1"> <div class="fzspu-row"> <div class="fzspu-i">i</div> <div class="fzspu-rbody"> <div class="fzspu-label">تصویر شاخص <span class="fzspu-req">*</span></div> <div class="fzspu-pickRow"> <div class="fzspu-photoBox" id="fzspuFeaturedBox"> <div class="fzspu-hint"><span class="fzspu-picon">📷</span>افزودن عکس از گالری گوشی</div> <img class="fzspu-preview" id="fzspuFeaturedPreview" alt=""> <div class="fzspu-editBtn" id="fzspuFeaturedEdit">ویرایش</div> <input type="file" accept="image/*" id="fzspuFeaturedInput"> </div> <button type="button" class="fzspu-libBtn" id="fzspuFeaturedFromLibrary">کتابخانه سایت</button> </div> <div class="fzspu-uploadline" id="fzspuFeaturedLine"> <div class="fzspu-bar"><div class="fzspu-fill" style="width:0%"></div></div> <div class="fzspu-pct">0%</div> </div> </div> </div> <div class="fzspu-row"> <div class="fzspu-i">i</div> <div class="fzspu-rbody"> <div class="fzspu-label">گالری (اختیاری)</div> <div class="fzspu-pickRow"> <div class="fzspu-photoBox fzspu-smallBox"> <div class="fzspu-hint"><span class="fzspu-picon">+</span>افزودن عکس از گالری گوشی</div> <input type="file" accept="image/*" id="fzspuGalleryInput" multiple> </div> <button type="button" class="fzspu-libBtn" id="fzspuGalleryFromLibrary">کتابخانه سایت</button> </div> <div class="fzspu-thumbs" id="fzspuGalleryThumbs"></div> </div> </div> <div class="fzspu-row"> <div class="fzspu-i">i</div> <div class="fzspu-rbody"> <div class="fzspu-label">عنوان <span class="fzspu-req">*</span></div> <input id="fzspuTitle" type="text" placeholder="عنوان آگهی خود را بنویسید"> </div> </div> <div class="fzspu-row"> <div class="fzspu-i">i</div> <div class="fzspu-rbody"> <div class="fzspu-label">توضیحات <span class="fzspu-req">*</span></div> <textarea id="fzspuDesc" placeholder="توضیحات مربوط به آگهی را بنویسید"></textarea> </div> </div> <div class="fzspu-row"> <div class="fzspu-i">i</div> <div class="fzspu-rbody"> <div class="fzspu-label">قیمت (تومان) <span class="fzspu-req">*</span></div> <input id="fzspuPrice" type="text" inputmode="numeric" placeholder="مثال: 1,250,000"> <div class="fzspu-subhint">سه‌تا سه‌تا جدا می‌شود (نمایشی). موقع ثبت، عدد واقعی بدون ویرگول ذخیره می‌شود.</div> </div> </div> <div class="fzspu-row"> <div class="fzspu-i">i</div> <div class="fzspu-rbody"> <div class="fzspu-label">دسته‌بندی <span class="fzspu-req">*</span></div> <select id="fzspuCat" class="fzspu-select"><option value="">انتخاب دسته‌بندی</option></select> </div> </div> <div class="fzspu-bottom"> <button class="fzspu-next" id="fzspuSubmit" type="button">ثبت نهایی</button> <div class="fzspu-status" id="fzspuStatus"></div> <div class="fzspu-debug" id="fzspuDebug"></div> </div> </div> </div> <div class="fzspu-modal" id="fzspuModal" aria-hidden="true"> <div class="fzspu-sheet"> <div class="fzspu-sheetTop"> <div class="fzspu-sheetTitle">ویرایش عکس</div> <div class="fzspu-iconbtn" id="fzspuModalClose">×</div> </div> <div class="fzspu-editArea"> <div class="fzspu-fakeCanvas" id="fzspuFakeCanvas">پیش‌نمایش (UI)</div> <div class="fzspu-toolbar"> <button type="button" class="fzspu-toolbtn">کراپ</button> <button type="button" class="fzspu-toolbtn">نور</button> <button type="button" class="fzspu-toolbtn">رنگ</button> </div> <div class="fzspu-sliders"> <div class="fzspu-srow"><div>نور</div><input type="range" min="0" max="100" value="50"><div>50%</div></div> <div class="fzspu-srow"><div>کنتراست</div><input type="range" min="0" max="100" value="50"><div>50%</div></div> <div class="fzspu-srow"><div>رنگ</div><input type="range" min="0" max="100" value="50"><div>50%</div></div> </div> <div class="fzspu-captionWrap"><input id="fzspuCaption" class="fzspu-caption" type="text" placeholder="توضیحات را اضافه کنید..." /></div> </div> <div class="fzspu-sheetBottom"> <button type="button" class="fzspu-btnGhost" id="fzspuModalCancel">انصراف</button> <button type="button" class="fzspu-btnPrimary" id="fzspuModalSave">ذخیره</button> </div> </div> </div> <div class="fzspu-popup" id="fzspuSuccess" aria-hidden="true"> <div class="fzspu-popcard"> <div class="fzspu-check">✓</div> <div class="fzspu-poptitle">با موفقیت ثبت شد</div> <div class="fzspu-poptext">در سایت نمایش داده می‌شود.</div> <div class="fzspu-popactions"> <a class="fzspu-popbtn" id="fzspuViewBtn" href="#" target="_blank" rel="noopener">مشاهده محصول</a> <button class="fzspu-popbtn ghost" type="button" id="fzspuCloseSuccess">بستن</button> </div> </div> </div> </div> <?php return ob_get_clean(); } function fzspu_css() { return <<<CSS :root{--fzbg:#fff;--fzline:#e9e9ee;--fztext:#111;--fzmuted:#777;--fzred:#c62828;--fzgreen:#1b8f3a;--fzfield:#fdfdfd;--fzshadow:0 12px 30px rgba(0,0,0,.10);--fzradius:14px;} .fzspu-app{background:var(--fzbg);color:var(--fztext);font-family:Tahoma,system-ui,-apple-system,Segoe UI,Roboto,Arial;} .fzspu-topbar{height:56px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;border-bottom:1px solid var(--fzline);position:sticky;top:0;background:#fff;z-index:10;} .fzspu-title{font-weight:800;font-size:18px;} .fzspu-iconbtn{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;color:#555;user-select:none;} .fzspu-iconbtn:active{background:#f3f3f7} .fzspu-wrap{max-width:560px;margin:0 auto;} .fzspu-row{display:flex;gap:12px;padding:16px 14px;align-items:flex-start;border-top:1px solid var(--fzline);} .fzspu-row:first-child{border-top:none} .fzspu-i{width:24px;height:24px;border-radius:99px;border:1px solid #cfcfd6;color:#7b7b86;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;margin-top:2px;flex:0 0 24px;} .fzspu-rbody{flex:1} .fzspu-label{font-size:18px;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:8px;} .fzspu-req{color:#d32f2f} .fzspu-subhint{font-size:13px;color:var(--fzmuted);margin-top:10px;line-height:1.6} .fzspu-app input,.fzspu-app textarea,.fzspu-app select{width:100%;border:1px solid #d9d9e1;border-radius:var(--fzradius);padding:14px 12px;font-size:16px;background:var(--fzfield);outline:none;} .fzspu-app textarea{min-height:120px;resize:none;} .fzspu-pickRow{display:flex;gap:10px;align-items:center;justify-content:flex-start;} .fzspu-libBtn{border:1px solid #e2e2e8;background:#fafafa;color:#777;border-radius:12px;padding:10px 10px;font-weight:800;font-size:12px;min-width:96px;height:44px;opacity:.75;} .fzspu-libBtn:active{background:#f1f1f5} .fzspu-photoBox{width:150px;height:150px;border:2px dashed #cfcfd6;border-radius:var(--fzradius);display:flex;align-items:center;justify-content:center;text-align:center;color:#777;user-select:none;position:relative;overflow:hidden;background:#fff;} .fzspu-smallBox{width:150px;height:90px;} .fzspu-picon{font-size:30px;display:block;margin-bottom:6px} .fzspu-photoBox input{opacity:0;position:absolute;inset:0;cursor:pointer} .fzspu-preview{display:none;width:100%;height:100%;object-fit:cover} .fzspu-photoBox.hasImg .fzspu-hint{display:none} .fzspu-photoBox.hasImg .fzspu-preview{display:block} .fzspu-hint{font-size:14px;line-height:1.3} .fzspu-editBtn{position:absolute;left:8px;top:8px;background:rgba(0,0,0,.55);color:#fff;padding:6px 10px;border-radius:10px;font-size:12px;font-weight:800;display:none;} .fzspu-photoBox.hasImg .fzspu-editBtn{display:block;} .fzspu-bottom{position:sticky;bottom:0;padding:14px 14px 18px;background:#fff;border-top:1px solid var(--fzline);} .fzspu-next{width:100%;border:0;border-radius:12px;padding:14px 16px;background:var(--fzred);color:#fff;font-size:18px;font-weight:800;cursor:pointer;} .fzspu-next:active{transform:scale(.995);filter:brightness(.97);} .fzspu-next[disabled]{opacity:.6;cursor:not-allowed} .fzspu-status,.fzspu-debug{margin-top:10px;font-size:13px;color:#222;word-break:break-word;} .fzspu-debug{color:#666} .fzspu-screen{display:none;} .fzspu-screen.active{display:block;} .fzspu-select{appearance:auto;background:#fff;padding-left:12px;} .fzspu-uploadline{margin-top:10px;display:flex;align-items:center;gap:10px;} .fzspu-bar{flex:1;height:6px;background:#ececf2;border-radius:99px;overflow:hidden;} .fzspu-fill{height:100%;width:0%;background:var(--fzred);border-radius:99px;transition:width .08s linear;} .fzspu-pct{font-size:12px;color:var(--fzmuted);width:40px;text-align:left;direction:ltr;} .fzspu-thumbs{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px;} .fzspu-thumbWrap{width:74px;display:flex;flex-direction:column;gap:6px;} .fzspu-thumb{width:74px;height:74px;border-radius:14px;overflow:hidden;border:1px solid #d9d9e1;background:#fff;position:relative;} .fzspu-thumb img{width:100%;height:100%;object-fit:cover;display:block} .fzspu-thumbName{font-size:11px;color:#555;line-height:1.2;text-align:center;max-width:74px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} .fzspu-thumbLine{display:flex;align-items:center;gap:6px;} .fzspu-thumbLine .fzspu-bar{height:5px;} .fzspu-thumbLine .fzspu-pct{width:34px;font-size:11px;} .fzspu-thumbEdit{position:absolute;left:6px;top:6px;background:rgba(0,0,0,.55);color:#fff;border-radius:10px;padding:6px 8px;font-size:12px;font-weight:800;} .fzspu-descGallery{margin-top:14px;padding:12px;border:1px solid #ececf2;border-radius:16px;background:#fff;} .fzspu-descGalleryTitle{font-weight:900;margin-bottom:10px;font-size:14px;} .fzspu-descGalleryGrid{display:flex;gap:10px;flex-wrap:wrap;} .fzspu-dgItem{width:92px;display:flex;flex-direction:column;gap:6px} .fzspu-dgThumb{width:92px;height:92px;border-radius:14px;overflow:hidden;border:1px solid #d9d9e1;background:#fff;} .fzspu-dgThumb img{width:100%;height:100%;object-fit:cover;display:block;} .fzspu-dgName{font-size:11px;color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;max-width:92px;} .fzspu-modal{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:flex-end;justify-content:center;z-index:99999;} .fzspu-modal.show{display:flex;} .fzspu-sheet{width:min(560px,100%);background:#fff;border-radius:18px 18px 0 0;box-shadow:var(--fzshadow);overflow:hidden;} .fzspu-sheetTop{padding:12px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--fzline);} .fzspu-sheetTitle{font-weight:900} .fzspu-editArea{padding:12px;} .fzspu-fakeCanvas{width:100%;height:280px;border-radius:16px;border:1px solid #e7e7ee;background:linear-gradient(135deg,#f7f7fb 25%,transparent 25%) -10px 0/20px 20px,linear-gradient(225deg,#f7f7fb 25%,transparent 25%) -10px 0/20px 20px,linear-gradient(315deg,#f7f7fb 25%,transparent 25%) 0px 0/20px 20px,linear-gradient(45deg,#f7f7fb 25%,transparent 25%) 0px 0/20px 20px,#fff;display:flex;align-items:center;justify-content:center;color:#777;text-align:center;padding:12px;} .fzspu-toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;} .fzspu-toolbtn{border:1px solid #e2e2e8;background:#fff;border-radius:14px;padding:10px 12px;font-weight:900;font-size:13px;} .fzspu-toolbtn:active{background:#f3f3f7} .fzspu-sliders{margin-top:12px;display:grid;gap:10px;} .fzspu-srow{display:grid;grid-template-columns:90px 1fr 50px;align-items:center;gap:10px;font-size:13px;color:#666;} .fzspu-captionWrap{margin-top:12px;} .fzspu-caption{width:100%;border-radius:999px;padding:14px 16px;border:1px solid #e5e7eb;background:#f3f4f6;font-size:14px;} .fzspu-sheetBottom{padding:12px;border-top:1px solid var(--fzline);display:flex;gap:10px;} .fzspu-btnGhost{flex:1;border:1px solid var(--fzline);background:#fff;border-radius:14px;padding:12px;font-weight:900;} .fzspu-btnPrimary{flex:1;border:0;background:var(--fzred);color:#fff;border-radius:14px;padding:12px;font-weight:900;} .fzspu-popup{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:100000;} .fzspu-popup.show{display:flex;} .fzspu-popcard{width:min(420px,92vw);background:#fff;border-radius:18px;box-shadow:var(--fzshadow);padding:18px;text-align:center;} .fzspu-check{width:64px;height:64px;border-radius:999px;background:rgba(27,143,58,.12);color:var(--fzgreen);display:flex;align-items:center;justify-content:center;font-size:34px;font-weight:900;margin:6px auto 10px;} .fzspu-poptitle{font-size:18px;font-weight:900;margin-top:4px;} .fzspu-poptext{font-size:13px;color:var(--fzmuted);margin-top:6px;} .fzspu-popactions{display:flex;gap:10px;margin-top:14px;} .fzspu-popbtn{flex:1;border:0;border-radius:14px;padding:12px;font-weight:900;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;} .fzspu-popbtn{background:var(--fzred);color:#fff;} .fzspu-popbtn.ghost{background:#fff;color:#222;border:1px solid var(--fzline);} CSS; } function fzspu_js() { return <<<JS (function(){ const REST_NONCE = (window.FZSPU && window.FZSPU.restNonce) || ''; const AJAX_NONCE = (window.FZSPU && window.FZSPU.ajaxNonce) || ''; const AJAX_URL = (window.FZSPU && window.FZSPU.ajaxUrl) || ''; const API = '/wp-json/fzspu/v1'; const debugEl = document.getElementById('fzspuDebug'); function dbg(msg){ if(debugEl) debugEl.textContent = msg || ''; } // WordPress Media Library (front-end) function pickFromLibrary(opts){ return new Promise((resolve,reject)=>{ try{ if(!(window.wp && wp.media)) return reject('wp.media not available'); const frame = wp.media({ title: (opts && opts.title) ? opts.title : 'انتخاب تصویر', button: { text: (opts && opts.buttonText) ? opts.buttonText : 'انتخاب' }, library: { type: 'image' }, multiple: !!(opts && opts.multiple) }); frame.on('select', function(){ const sel = frame.state().get('selection'); const arr = []; sel.each(function(att){ const j = att.toJSON(); arr.push({ id: j.id, url: (j.sizes && j.sizes.large && j.sizes.large.url) ? j.sizes.large.url : j.url, thumb: (j.sizes && j.sizes.thumbnail && j.sizes.thumbnail.url) ? j.sizes.thumbnail.url : j.url, filename: j.filename || ('image-' + j.id + '.jpg') }); }); resolve(arr); }); frame.open(); }catch(e){ reject(e); } }); } const modal = document.getElementById('fzspuModal'); const fakeCanvas = document.getElementById('fzspuFakeCanvas'); const caption = document.getElementById('fzspuCaption'); function openEditor(label){ fakeCanvas.textContent = 'پیش‌نمایش (UI) — ' + (label || 'عکس'); caption.value = ''; modal.classList.add('show'); } function closeEditor(){ modal.classList.remove('show'); } document.getElementById('fzspuModalClose').addEventListener('click', closeEditor); document.getElementById('fzspuModalCancel').addEventListener('click', closeEditor); document.getElementById('fzspuModalSave').addEventListener('click', function(){ closeEditor(); alert('ذخیره شد (فعلاً فقط UI)'); }); const success = document.getElementById('fzspuSuccess'); const viewBtn = document.getElementById('fzspuViewBtn'); document.getElementById('fzspuCloseSuccess').addEventListener('click', function(){ success.classList.remove('show'); }); function showSuccess(url){ viewBtn.href = url || '#'; success.classList.add('show'); } const priceEl = document.getElementById('fzspuPrice'); function onlyDigits(s){ return (s||'').toString().replace(/[^0-9]/g,''); } function formatThousands(d){ d = onlyDigits(d); if(!d) return ''; return d.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','); } priceEl.addEventListener('input', function(){ const raw = onlyDigits(priceEl.value); priceEl.value = formatThousands(raw); }); function setLine(fillEl, pctEl, val){ const v = Math.max(0, Math.min(100, val|0)); fillEl.style.width = v + '%'; pctEl.textContent = v + '%'; } function xhrUploadRest(file, onProgress){ return new Promise((resolve, reject)=>{ const xhr = new XMLHttpRequest(); xhr.open('POST', API + '/upload', true); xhr.setRequestHeader('X-WP-Nonce', REST_NONCE); xhr.upload.onprogress = (e)=>{ if(e.lengthComputable) onProgress((e.loaded/e.total)*100); }; xhr.onreadystatechange = ()=>{ if(xhr.readyState === 4){ try{ const data = JSON.parse(xhr.responseText || '{}'); if(xhr.status >= 200 && xhr.status < 300 && data && data.ok) resolve(data); else reject({status:xhr.status, message:(data && data.message) ? data.message : (xhr.responseText||('HTTP '+xhr.status))}); }catch(e){ reject({status:xhr.status, message:'parse'}); } } }; const fd = new FormData(); fd.append('file', file); xhr.send(fd); }); } function xhrUploadAjax(file, onProgress){ return new Promise((resolve, reject)=>{ const xhr = new XMLHttpRequest(); xhr.open('POST', AJAX_URL, true); xhr.upload.onprogress = (e)=>{ if(e.lengthComputable) onProgress((e.loaded/e.total)*100); }; xhr.onreadystatechange = ()=>{ if(xhr.readyState === 4){ try{ const data = JSON.parse(xhr.responseText || '{}'); if(xhr.status >= 200 && xhr.status < 300 && data && data.ok) resolve(data); else reject({status:xhr.status, message:(data && data.message) ? data.message : (xhr.responseText||('HTTP '+xhr.status))}); }catch(e){ reject({status:xhr.status, message:'parse'}); } } }; const fd = new FormData(); fd.append('action','fzspu_upload'); fd.append('_ajax_nonce', AJAX_NONCE); fd.append('file', file); xhr.send(fd); }); } async function uploadSmart(file, onProgress){ try{ const r = await xhrUploadRest(file, onProgress); dbg('REST OK'); return r; }catch(err){ dbg('REST fail → AJAX'); return await xhrUploadAjax(file, onProgress); } } async function getCats(){ try{ const r = await fetch(API + '/cats', { headers:{'X-WP-Nonce': REST_NONCE} }); if(!r.ok) throw new Error('rest cats ' + r.status); dbg('cats: REST'); return await r.json(); }catch(e){ const fd = new FormData(); fd.append('action','fzspu_cats'); fd.append('_ajax_nonce', AJAX_NONCE); const r2 = await fetch(AJAX_URL, { method:'POST', body: fd }); dbg('cats: AJAX'); return await r2.json(); } } async function createProduct(payload){ try{ const r = await fetch(API + '/create', { method:'POST', headers:{'X-WP-Nonce': REST_NONCE, 'Content-Type':'application/json'}, body: JSON.stringify(payload) }); const d = await r.json(); if(!r.ok || !d || !d.ok) throw d; dbg('create: REST'); return d; }catch(e){ const fd = new FormData(); fd.append('action','fzspu_create'); fd.append('_ajax_nonce', AJAX_NONCE); fd.append('payload', JSON.stringify(payload)); const r2 = await fetch(AJAX_URL, { method:'POST', body: fd }); const d2 = await r2.json(); dbg('create: AJAX'); return d2; } } let featuredId = 0; let galleryIds = []; let galleryItems = []; let submitting = false; const featuredInput = document.getElementById('fzspuFeaturedInput'); const featuredFromLib = document.getElementById('fzspuFeaturedFromLibrary'); const featuredPreview = document.getElementById('fzspuFeaturedPreview'); const featuredBox = document.getElementById('fzspuFeaturedBox'); const submitBtn = document.getElementById('fzspuSubmit'); document.getElementById('fzspuFeaturedEdit').addEventListener('click', (e)=>{ e.preventDefault(); e.stopPropagation(); if (!featuredBox.classList.contains('hasImg')) return; openEditor('تصویر شاخص'); }); const featuredLine = document.getElementById('fzspuFeaturedLine'); const featuredFill = featuredLine.querySelector('.fzspu-fill'); const featuredPct = featuredLine.querySelector('.fzspu-pct'); if(featuredFromLib){ featuredFromLib.addEventListener('click', async ()=>{ try{ const arr = await pickFromLibrary({title:'انتخاب تصویر شاخص', multiple:false}); if(!arr || !arr.length) return; const a = arr[0]; featuredId = a.id || 0; featuredPreview.src = a.url; featuredBox.classList.add('hasImg'); setLine(featuredFill, featuredPct, 100); }catch(e){ alert('کتابخانه سایت در این صفحه فعال نیست.'); } }); } featuredInput.addEventListener('change', async (e)=>{ const f = e.target.files && e.target.files[0]; if(!f) return; featuredPreview.src = URL.createObjectURL(f); featuredBox.classList.add('hasImg'); setLine(featuredFill, featuredPct, 0); try{ const res = await uploadSmart(f, (pct)=>setLine(featuredFill, featuredPct, pct)); featuredId = res.attachment_id || 0; setLine(featuredFill, featuredPct, 100); }catch(err){ alert('آپلود تصویر شاخص ناموفق: ' + (err.message || err)); setLine(featuredFill, featuredPct, 0); featuredId = 0; } }); const galleryInput = document.getElementById('fzspuGalleryInput'); const galleryFromLib = document.getElementById('fzspuGalleryFromLibrary'); const thumbs = document.getElementById('fzspuGalleryThumbs'); function makeThumbWrap(item){ const wrap = document.createElement('div'); wrap.className='fzspu-thumbWrap'; const t = document.createElement('div'); t.className='fzspu-thumb'; const img = document.createElement('img'); img.src = item.url; t.appendChild(img); const edit = document.createElement('div'); edit.className='fzspu-thumbEdit'; edit.textContent='ویرایش'; edit.addEventListener('click',(ev)=>{ ev.preventDefault(); ev.stopPropagation(); openEditor(item.name || 'عکس'); }); t.appendChild(edit); const name = document.createElement('div'); name.className='fzspu-thumbName'; name.textContent=item.name || ''; const line = document.createElement('div'); line.className='fzspu-thumbLine'; const bar = document.createElement('div'); bar.className='fzspu-bar'; const fill = document.createElement('div'); fill.className='fzspu-fill'; bar.appendChild(fill); const pct = document.createElement('div'); pct.className='fzspu-pct'; pct.textContent='0%'; line.appendChild(bar); line.appendChild(pct); wrap.appendChild(t); wrap.appendChild(name); wrap.appendChild(line); return {wrap, fill, pct}; } if(galleryFromLib){ galleryFromLib.addEventListener('click', async ()=>{ try{ const arr = await pickFromLibrary({title:'انتخاب تصاویر گالری', multiple:true}); if(!arr || !arr.length) return; thumbs.innerHTML=''; galleryIds=[]; galleryItems=[]; arr.slice(0,12).forEach((a)=>{ if(a.id) galleryIds.push(a.id); galleryItems.push({source:'lib', name: a.filename, url: a.url}); const ui = makeThumbWrap({name:a.filename, url:a.thumb || a.url}); thumbs.appendChild(ui.wrap); setLine(ui.fill, ui.pct, 100); }); renderDescGallery(); }catch(e){ alert('کتابخانه سایت در این صفحه فعال نیست.'); } }); } galleryInput.addEventListener('change', async (e)=>{ thumbs.innerHTML=''; galleryIds=[]; galleryItems=[]; const files = Array.from(e.target.files||[]).slice(0,12); if(!files.length){ return; } galleryItems = files.map(f=>({source:'file', name:f.name, url: URL.createObjectURL(f), file:f})); for(const item of galleryItems){ const ui = makeThumbWrap({name:item.name, url:item.url}); thumbs.appendChild(ui.wrap); setLine(ui.fill, ui.pct, 0); try{ const res = await uploadSmart(item.file, (pct)=>setLine(ui.fill, ui.pct, pct)); const id = res.attachment_id || 0; if(id) galleryIds.push(id); setLine(ui.fill, ui.pct, 100); }catch(err){ alert('آپلود گالری ناموفق: ' + (err.message || err)); setLine(ui.fill, ui.pct, 0); } } }); (async function(){ const sel = document.getElementById('fzspuCat'); try{ const cats = await getCats(); sel.innerHTML = '<option value="" selected disabled>انتخاب دسته‌بندی</option>'; (cats||[]).forEach(c=>{ const opt = document.createElement('option'); opt.value = c.id; opt.textContent = c.name; sel.appendChild(opt); }); }catch(e){ sel.innerHTML = '<option value="">خطا در بارگذاری</option>'; } })(); const status = document.getElementById('fzspuStatus'); function lockSubmit(){ submitting = true; submitBtn.disabled = true; submitBtn.textContent = 'در حال ثبت...'; } function unlockSubmit(){ submitting = false; submitBtn.disabled = false; submitBtn.textContent = 'ثبت نهایی'; } function doneSubmit(){ submitting = true; submitBtn.disabled = true; submitBtn.textContent = 'ثبت شد ✓'; } submitBtn.addEventListener('click', async ()=>{ if (submitting) return; lockSubmit(); status.textContent = 'در حال ثبت...'; const title = (document.getElementById('fzspuTitle').value||'').trim(); const desc = (document.getElementById('fzspuDesc').value||'').trim(); const priceRaw = (document.getElementById('fzspuPrice').value||''); const price = onlyDigits(priceRaw); const catId = (document.getElementById('fzspuCat').value||'').trim(); if(!featuredId){ status.textContent='تصویر شاخص را آپلود کن.'; unlockSubmit(); return; } if(!title){ status.textContent='عنوان را وارد کن.'; unlockSubmit(); return; } if(!desc){ status.textContent='توضیحات را وارد کن.'; unlockSubmit(); return; } if(!price){ status.textContent='قیمت را وارد کن.'; unlockSubmit(); return; } if(!catId){ status.textContent='دسته‌بندی را انتخاب کن.'; unlockSubmit(); return; } const payload = { title, description: desc, price, category_id: catId, featured_id: featuredId, gallery_ids: galleryIds }; try{ const data = await createProduct(payload); if(data && data.ok){ status.textContent = ''; doneSubmit(); showSuccess(data.view_url || '#'); } else { status.textContent = '❌ خطا: ' + (data && data.message ? data.message : 'نامشخص'); unlockSubmit(); } }catch(e){ status.textContent = '❌ خطا'; unlockSubmit(); } }); })(); JS; } /** REST routes **/ add_action('rest_api_init', function () { register_rest_route('fzspu/v1', '/cats', [ 'methods' => 'GET', 'permission_callback' => function () { return fzspu_can_use(); }, 'callback' => function () { $terms = get_terms(['taxonomy'=>'product_cat','hide_empty'=>false]); $out = []; foreach ($terms as $t) $out[] = ['id'=>$t->term_id, 'name'=>$t->name]; return new WP_REST_Response($out, 200); } ]); register_rest_route('fzspu/v1', '/upload', [ 'methods' => 'POST', 'permission_callback' => function () { return is_user_logged_in() && current_user_can('upload_files'); }, 'callback' => function () { return fzspu_do_upload(); } ]); register_rest_route('fzspu/v1', '/create', [ 'methods' => 'POST', 'permission_callback' => function () { return fzspu_can_use(); }, 'callback' => function (WP_REST_Request $req) { return fzspu_do_create($req->get_params()); } ]); }); /** AJAX fallbacks **/ add_action('wp_ajax_fzspu_upload', function(){ check_ajax_referer('fzspu_ajax'); $r = fzspu_do_upload(); wp_send_json($r, (!empty($r['ok'])) ? 200 : 400); }); add_action('wp_ajax_fzspu_cats', function(){ check_ajax_referer('fzspu_ajax'); if(!fzspu_can_use()) wp_send_json(['ok'=>false,'message'=>'no permission'], 403); $terms = get_terms(['taxonomy'=>'product_cat','hide_empty'=>false]); $out = []; foreach ($terms as $t) $out[] = ['id'=>$t->term_id, 'name'=>$t->name]; wp_send_json($out, 200); }); add_action('wp_ajax_fzspu_create', function(){ check_ajax_referer('fzspu_ajax'); if(!fzspu_can_use()) wp_send_json(['ok'=>false,'message'=>'no permission'], 403); $payload = isset($_POST['payload']) ? json_decode(stripslashes($_POST['payload']), true) : []; $r = fzspu_do_create($payload); wp_send_json($r, (!empty($r['ok'])) ? 200 : 400); }); function fzspu_do_upload(){ if (empty($_FILES['file'])) return ['ok'=>false,'message'=>'no file']; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $file = $_FILES['file']; $overrides = ['test_form' => false]; $movefile = wp_handle_upload($file, $overrides); if (!$movefile || isset($movefile['error'])) { return ['ok'=>false,'message'=> ($movefile['error'] ?? 'upload error')]; } $filename = $movefile['file']; $filetype = wp_check_filetype(basename($filename), null); $attachment = [ 'post_mime_type' => $filetype['type'], 'post_title' => sanitize_file_name(basename($filename)), 'post_content' => '', 'post_status' => 'inherit' ]; $attach_id = wp_insert_attachment($attachment, $filename); $attach_data = wp_generate_attachment_metadata($attach_id, $filename); wp_update_attachment_metadata($attach_id, $attach_data); return ['ok'=>true,'attachment_id'=>(int)$attach_id,'url'=>wp_get_attachment_url($attach_id)]; } function fzspu_set_image_seo($attach_id, $base_title, $index = null){ $attach_id = (int)$attach_id; if(!$attach_id) return; $suffix = ($index !== null) ? (' – تصویر ' . intval($index)) : ''; $val = trim($base_title . $suffix); update_post_meta($attach_id, '_wp_attachment_image_alt', $val); wp_update_post([ 'ID' => $attach_id, 'post_title' => $val, 'post_excerpt' => $val, 'post_content' => $val, ]); } function fzspu_build_gallery_html($gallery_ids, $base_title){ $gallery_ids = array_values(array_filter(array_map('intval', (array)$gallery_ids))); if(!$gallery_ids) return ''; $html = "\n\n<hr />\n<div class=\"fzspu-desc-gallery\">\n"; $i = 1; foreach($gallery_ids as $aid){ $url = wp_get_attachment_image_url($aid, 'large'); if(!$url) continue; $alt = esc_attr(trim($base_title . ' – تصویر ' . $i)); $cap = esc_html(trim($base_title . ' – تصویر ' . $i)); $html .= "<figure class=\"fzspu-fig\"><img src=\"" . esc_url($url) . "\" alt=\"{$alt}\" loading=\"lazy\" decoding=\"async\" /><figcaption>{$cap}</figcaption></figure>\n"; $i++; } $html .= "</div>\n"; return $html; } function fzspu_do_create($params){ if (!class_exists('WooCommerce')) return ['ok'=>false,'message'=>'WooCommerce not active']; $title = isset($params['title']) ? sanitize_text_field($params['title']) : ''; $description = isset($params['description']) ? wp_kses_post($params['description']) : ''; $price = isset($params['price']) ? wc_format_decimal($params['price']) : ''; $cat_id = isset($params['category_id']) ? (int)$params['category_id'] : 0; $featured_id = isset($params['featured_id']) ? (int)$params['featured_id'] : 0; $gallery_ids = (isset($params['gallery_ids']) && is_array($params['gallery_ids'])) ? $params['gallery_ids'] : []; if (!$title || !$description || $price === '' || !$cat_id || !$featured_id) { return ['ok'=>false,'message'=>'missing fields']; } // SEO for images fzspu_set_image_seo($featured_id, $title, null); $gallery_ids = array_values(array_filter(array_map('intval', $gallery_ids))); $idx = 1; foreach($gallery_ids as $aid){ fzspu_set_image_seo($aid, $title, $idx); $idx++; } // Put title inside description as H2 (at top) $h2 = '<h2>' . esc_html($title) . '</h2>'; $full_description = $h2 . "\n" . $description . fzspu_build_gallery_html($gallery_ids, $title); $product_id = wp_insert_post([ 'post_type' => 'product', 'post_status' => 'publish', 'post_title' => $title, 'post_content' => $full_description, ], true); if (is_wp_error($product_id)) return ['ok'=>false,'message'=>$product_id->get_error_message()]; wp_set_object_terms($product_id, [$cat_id], 'product_cat'); wp_set_object_terms($product_id, 'simple', 'product_type'); update_post_meta($product_id, '_regular_price', $price); update_post_meta($product_id, '_price', $price); set_post_thumbnail($product_id, $featured_id); if ($gallery_ids) update_post_meta($product_id, '_product_image_gallery', implode(',', $gallery_ids)); return [ 'ok'=>true, 'product_id'=>(int)$product_id, 'edit_url'=>admin_url('post.php?post=' . $product_id . '&action=edit'), 'view_url'=>get_permalink($product_id), ]; }
<?php
/**
 * Plugin Name: Faryazan Simple Product Uploader
 * Description: Mobile-first uploader for WooCommerce. REST + AJAX fallback. Upload progress 0-100. Price display with thousands separators (visual only). SEO meta for images + gallery images appended to description. Title appended as H2 in description.
 * Version: 1.1.0
 * Author: Faryazan
 */
if (!defined('ABSPATH')) exit;

add_action('init', function () {
  add_shortcode('faryazan_simple_uploader', 'fzspu_shortcode');
});

function fzspu_can_use() {
  return is_user_logged_in() && (current_user_can('manage_woocommerce') || current_user_can('administrator'));
}

add_action('wp_enqueue_scripts', function () {
  if (!is_singular()) return;
  global $post;
  if (!$post || (!has_shortcode($post->post_content, 'faryazan_simple_uploader') && !has_shortcode($post->post_content, 'fzspu'))) return;
  if (!fzspu_can_use()) return;

  $ver = '1.0.8';

  wp_register_style('fzspu_css', false, [], $ver);
  wp_enqueue_style('fzspu_css');
  wp_add_inline_style('fzspu_css', fzspu_css());

    // Needed for WordPress Media Library picker on front-end
  if (function_exists('wp_enqueue_media')) { wp_enqueue_media(); }
wp_register_script('fzspu_js', false, ['jquery'], $ver, true);
  wp_enqueue_script('fzspu_js');

  wp_add_inline_script('fzspu_js', 'window.FZSPU = ' . wp_json_encode([
    'restNonce' => wp_create_nonce('wp_rest'),
    'ajaxNonce' => wp_create_nonce('fzspu_ajax'),
    'ajaxUrl'   => admin_url('admin-ajax.php'),
  ]) . ';', 'before');

  wp_add_inline_script('fzspu_js', fzspu_js(), 'after');
});

function fzspu_shortcode() {
  if (!fzspu_can_use()) {
    return '<div style="padding:16px;font-family:tahoma">برای استفاده باید با اکانت مدیر وارد شوید.</div>';
  }

  ob_start(); ?>
  <div class="fzspu-app" dir="rtl">
    <div class="fzspu-topbar">
      <div class="fzspu-iconbtn" data-action="close">×</div>
      <div class="fzspu-title">ثبت آگهی</div>
      <div class="fzspu-iconbtn" id="fzspuBack" style="visibility:hidden;">→</div>
    </div>

  
  <div class="fzspu-wrap">
    <div class="fzspu-screen active" id="fzspuS1">

      <div class="fzspu-row">
        <div class="fzspu-i">i</div>
        <div class="fzspu-rbody">
          <div class="fzspu-label">تصویر شاخص <span class="fzspu-req">*</span></div>

          <div class="fzspu-pickRow">
            <div class="fzspu-photoBox" id="fzspuFeaturedBox">
              <div class="fzspu-hint"><span class="fzspu-picon">📷</span>افزودن عکس از گالری گوشی</div>
              <img class="fzspu-preview" id="fzspuFeaturedPreview" alt="">
              <div class="fzspu-editBtn" id="fzspuFeaturedEdit">ویرایش</div>
              <input type="file" accept="image/*" id="fzspuFeaturedInput">
            </div>

            <button type="button" class="fzspu-libBtn" id="fzspuFeaturedFromLibrary">کتابخانه سایت</button>
          </div>

          <div class="fzspu-uploadline" id="fzspuFeaturedLine">
            <div class="fzspu-bar"><div class="fzspu-fill" style="width:0%"></div></div>
            <div class="fzspu-pct">0%</div>
          </div>
</div>
      </div>

      <div class="fzspu-row">
        <div class="fzspu-i">i</div>
        <div class="fzspu-rbody">
          <div class="fzspu-label">گالری (اختیاری)</div>

          <div class="fzspu-pickRow">
            <div class="fzspu-photoBox fzspu-smallBox">
              <div class="fzspu-hint"><span class="fzspu-picon">+</span>افزودن عکس از گالری گوشی</div>
              <input type="file" accept="image/*" id="fzspuGalleryInput" multiple>
            </div>

            <button type="button" class="fzspu-libBtn" id="fzspuGalleryFromLibrary">کتابخانه سایت</button>
          </div>

          <div class="fzspu-thumbs" id="fzspuGalleryThumbs"></div>
        </div>
      </div>

      <div class="fzspu-row">
        <div class="fzspu-i">i</div>
        <div class="fzspu-rbody">
          <div class="fzspu-label">عنوان <span class="fzspu-req">*</span></div>
          <input id="fzspuTitle" type="text" placeholder="عنوان آگهی خود را بنویسید">
        </div>
      </div>

      <div class="fzspu-row">
        <div class="fzspu-i">i</div>
        <div class="fzspu-rbody">
          <div class="fzspu-label">توضیحات <span class="fzspu-req">*</span></div>
          <textarea id="fzspuDesc" placeholder="توضیحات مربوط به آگهی را بنویسید"></textarea>
          </div>
        </div>



      <div class="fzspu-row">
        <div class="fzspu-i">i</div>
        <div class="fzspu-rbody">
          <div class="fzspu-label">قیمت (تومان) <span class="fzspu-req">*</span></div>
          <input id="fzspuPrice" type="text" inputmode="numeric" placeholder="مثال: 1,250,000">
          <div class="fzspu-subhint">سه‌تا سه‌تا جدا می‌شود (نمایشی). موقع ثبت، عدد واقعی بدون ویرگول ذخیره می‌شود.</div>
        </div>
      </div>

      <div class="fzspu-row">
        <div class="fzspu-i">i</div>
        <div class="fzspu-rbody">
          <div class="fzspu-label">دسته‌بندی <span class="fzspu-req">*</span></div>
          <select id="fzspuCat" class="fzspu-select"><option value="">انتخاب دسته‌بندی</option></select>
        </div>
      </div>

      <div class="fzspu-bottom">
        <button class="fzspu-next" id="fzspuSubmit" type="button">ثبت نهایی</button>
        <div class="fzspu-status" id="fzspuStatus"></div>
        <div class="fzspu-debug" id="fzspuDebug"></div>
      </div>
    </div>
  </div>
    <div class="fzspu-modal" id="fzspuModal" aria-hidden="true">
      <div class="fzspu-sheet">
        <div class="fzspu-sheetTop">
          <div class="fzspu-sheetTitle">ویرایش عکس</div>
          <div class="fzspu-iconbtn" id="fzspuModalClose">×</div>
        </div>
        <div class="fzspu-editArea">
          <div class="fzspu-fakeCanvas" id="fzspuFakeCanvas">پیش‌نمایش (UI)</div>
          <div class="fzspu-toolbar">
            <button type="button" class="fzspu-toolbtn">کراپ</button>
            <button type="button" class="fzspu-toolbtn">نور</button>
            <button type="button" class="fzspu-toolbtn">رنگ</button>
          </div>
          <div class="fzspu-sliders">
            <div class="fzspu-srow"><div>نور</div><input type="range" min="0" max="100" value="50"><div>50%</div></div>
            <div class="fzspu-srow"><div>کنتراست</div><input type="range" min="0" max="100" value="50"><div>50%</div></div>
            <div class="fzspu-srow"><div>رنگ</div><input type="range" min="0" max="100" value="50"><div>50%</div></div>
          </div>
          <div class="fzspu-captionWrap"><input id="fzspuCaption" class="fzspu-caption" type="text" placeholder="توضیحات را اضافه کنید..." /></div>
        </div>
        <div class="fzspu-sheetBottom">
          <button type="button" class="fzspu-btnGhost" id="fzspuModalCancel">انصراف</button>
          <button type="button" class="fzspu-btnPrimary" id="fzspuModalSave">ذخیره</button>
        </div>
      </div>
    </div>

    <div class="fzspu-popup" id="fzspuSuccess" aria-hidden="true">
      <div class="fzspu-popcard">
        <div class="fzspu-check">✓</div>
        <div class="fzspu-poptitle">با موفقیت ثبت شد</div>
        <div class="fzspu-poptext">در سایت نمایش داده می‌شود.</div>
        <div class="fzspu-popactions">
          <a class="fzspu-popbtn" id="fzspuViewBtn" href="#" target="_blank" rel="noopener">مشاهده محصول</a>
          <button class="fzspu-popbtn ghost" type="button" id="fzspuCloseSuccess">بستن</button>
        </div>
      </div>
    </div>

  </div>
  <?php
  return ob_get_clean();
}

function fzspu_css() { return <<<CSS
:root{--fzbg:#fff;--fzline:#e9e9ee;--fztext:#111;--fzmuted:#777;--fzred:#c62828;--fzgreen:#1b8f3a;--fzfield:#fdfdfd;--fzshadow:0 12px 30px rgba(0,0,0,.10);--fzradius:14px;}
.fzspu-app{background:var(--fzbg);color:var(--fztext);font-family:Tahoma,system-ui,-apple-system,Segoe UI,Roboto,Arial;}
.fzspu-topbar{height:56px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;border-bottom:1px solid var(--fzline);position:sticky;top:0;background:#fff;z-index:10;}
.fzspu-title{font-weight:800;font-size:18px;}
.fzspu-iconbtn{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;color:#555;user-select:none;}
.fzspu-iconbtn:active{background:#f3f3f7}
.fzspu-wrap{max-width:560px;margin:0 auto;}
.fzspu-row{display:flex;gap:12px;padding:16px 14px;align-items:flex-start;border-top:1px solid var(--fzline);}
.fzspu-row:first-child{border-top:none}
.fzspu-i{width:24px;height:24px;border-radius:99px;border:1px solid #cfcfd6;color:#7b7b86;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;margin-top:2px;flex:0 0 24px;}
.fzspu-rbody{flex:1}
.fzspu-label{font-size:18px;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:8px;}
.fzspu-req{color:#d32f2f}
.fzspu-subhint{font-size:13px;color:var(--fzmuted);margin-top:10px;line-height:1.6}
.fzspu-app input,.fzspu-app textarea,.fzspu-app select{width:100%;border:1px solid #d9d9e1;border-radius:var(--fzradius);padding:14px 12px;font-size:16px;background:var(--fzfield);outline:none;}
.fzspu-app textarea{min-height:120px;resize:none;}
.fzspu-pickRow{display:flex;gap:10px;align-items:center;justify-content:flex-start;}
.fzspu-libBtn{border:1px solid #e2e2e8;background:#fafafa;color:#777;border-radius:12px;padding:10px 10px;font-weight:800;font-size:12px;min-width:96px;height:44px;opacity:.75;}
.fzspu-libBtn:active{background:#f1f1f5}
.fzspu-photoBox{width:150px;height:150px;border:2px dashed #cfcfd6;border-radius:var(--fzradius);display:flex;align-items:center;justify-content:center;text-align:center;color:#777;user-select:none;position:relative;overflow:hidden;background:#fff;}
.fzspu-smallBox{width:150px;height:90px;}
.fzspu-picon{font-size:30px;display:block;margin-bottom:6px}
.fzspu-photoBox input{opacity:0;position:absolute;inset:0;cursor:pointer}
.fzspu-preview{display:none;width:100%;height:100%;object-fit:cover}
.fzspu-photoBox.hasImg .fzspu-hint{display:none}
.fzspu-photoBox.hasImg .fzspu-preview{display:block}
.fzspu-hint{font-size:14px;line-height:1.3}
.fzspu-editBtn{position:absolute;left:8px;top:8px;background:rgba(0,0,0,.55);color:#fff;padding:6px 10px;border-radius:10px;font-size:12px;font-weight:800;display:none;}
.fzspu-photoBox.hasImg .fzspu-editBtn{display:block;}
.fzspu-bottom{position:sticky;bottom:0;padding:14px 14px 18px;background:#fff;border-top:1px solid var(--fzline);}
.fzspu-next{width:100%;border:0;border-radius:12px;padding:14px 16px;background:var(--fzred);color:#fff;font-size:18px;font-weight:800;cursor:pointer;}
.fzspu-next:active{transform:scale(.995);filter:brightness(.97);}
.fzspu-next[disabled]{opacity:.6;cursor:not-allowed}
.fzspu-status,.fzspu-debug{margin-top:10px;font-size:13px;color:#222;word-break:break-word;}
.fzspu-debug{color:#666}
.fzspu-screen{display:none;}
.fzspu-screen.active{display:block;}
.fzspu-select{appearance:auto;background:#fff;padding-left:12px;}
.fzspu-uploadline{margin-top:10px;display:flex;align-items:center;gap:10px;}
.fzspu-bar{flex:1;height:6px;background:#ececf2;border-radius:99px;overflow:hidden;}
.fzspu-fill{height:100%;width:0%;background:var(--fzred);border-radius:99px;transition:width .08s linear;}
.fzspu-pct{font-size:12px;color:var(--fzmuted);width:40px;text-align:left;direction:ltr;}
.fzspu-thumbs{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px;}
.fzspu-thumbWrap{width:74px;display:flex;flex-direction:column;gap:6px;}
.fzspu-thumb{width:74px;height:74px;border-radius:14px;overflow:hidden;border:1px solid #d9d9e1;background:#fff;position:relative;}
.fzspu-thumb img{width:100%;height:100%;object-fit:cover;display:block}
.fzspu-thumbName{font-size:11px;color:#555;line-height:1.2;text-align:center;max-width:74px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.fzspu-thumbLine{display:flex;align-items:center;gap:6px;}
.fzspu-thumbLine .fzspu-bar{height:5px;}
.fzspu-thumbLine .fzspu-pct{width:34px;font-size:11px;}
.fzspu-thumbEdit{position:absolute;left:6px;top:6px;background:rgba(0,0,0,.55);color:#fff;border-radius:10px;padding:6px 8px;font-size:12px;font-weight:800;}
.fzspu-descGallery{margin-top:14px;padding:12px;border:1px solid #ececf2;border-radius:16px;background:#fff;}
.fzspu-descGalleryTitle{font-weight:900;margin-bottom:10px;font-size:14px;}
.fzspu-descGalleryGrid{display:flex;gap:10px;flex-wrap:wrap;}
.fzspu-dgItem{width:92px;display:flex;flex-direction:column;gap:6px}
.fzspu-dgThumb{width:92px;height:92px;border-radius:14px;overflow:hidden;border:1px solid #d9d9e1;background:#fff;}
.fzspu-dgThumb img{width:100%;height:100%;object-fit:cover;display:block;}
.fzspu-dgName{font-size:11px;color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;max-width:92px;}
.fzspu-modal{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:flex-end;justify-content:center;z-index:99999;}
.fzspu-modal.show{display:flex;}
.fzspu-sheet{width:min(560px,100%);background:#fff;border-radius:18px 18px 0 0;box-shadow:var(--fzshadow);overflow:hidden;}
.fzspu-sheetTop{padding:12px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--fzline);}
.fzspu-sheetTitle{font-weight:900}
.fzspu-editArea{padding:12px;}
.fzspu-fakeCanvas{width:100%;height:280px;border-radius:16px;border:1px solid #e7e7ee;background:linear-gradient(135deg,#f7f7fb 25%,transparent 25%) -10px 0/20px 20px,linear-gradient(225deg,#f7f7fb 25%,transparent 25%) -10px 0/20px 20px,linear-gradient(315deg,#f7f7fb 25%,transparent 25%) 0px 0/20px 20px,linear-gradient(45deg,#f7f7fb 25%,transparent 25%) 0px 0/20px 20px,#fff;display:flex;align-items:center;justify-content:center;color:#777;text-align:center;padding:12px;}
.fzspu-toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;}
.fzspu-toolbtn{border:1px solid #e2e2e8;background:#fff;border-radius:14px;padding:10px 12px;font-weight:900;font-size:13px;}
.fzspu-toolbtn:active{background:#f3f3f7}
.fzspu-sliders{margin-top:12px;display:grid;gap:10px;}
.fzspu-srow{display:grid;grid-template-columns:90px 1fr 50px;align-items:center;gap:10px;font-size:13px;color:#666;}
.fzspu-captionWrap{margin-top:12px;}
.fzspu-caption{width:100%;border-radius:999px;padding:14px 16px;border:1px solid #e5e7eb;background:#f3f4f6;font-size:14px;}
.fzspu-sheetBottom{padding:12px;border-top:1px solid var(--fzline);display:flex;gap:10px;}
.fzspu-btnGhost{flex:1;border:1px solid var(--fzline);background:#fff;border-radius:14px;padding:12px;font-weight:900;}
.fzspu-btnPrimary{flex:1;border:0;background:var(--fzred);color:#fff;border-radius:14px;padding:12px;font-weight:900;}
.fzspu-popup{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:100000;}
.fzspu-popup.show{display:flex;}
.fzspu-popcard{width:min(420px,92vw);background:#fff;border-radius:18px;box-shadow:var(--fzshadow);padding:18px;text-align:center;}
.fzspu-check{width:64px;height:64px;border-radius:999px;background:rgba(27,143,58,.12);color:var(--fzgreen);display:flex;align-items:center;justify-content:center;font-size:34px;font-weight:900;margin:6px auto 10px;}
.fzspu-poptitle{font-size:18px;font-weight:900;margin-top:4px;}
.fzspu-poptext{font-size:13px;color:var(--fzmuted);margin-top:6px;}
.fzspu-popactions{display:flex;gap:10px;margin-top:14px;}
.fzspu-popbtn{flex:1;border:0;border-radius:14px;padding:12px;font-weight:900;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;}
.fzspu-popbtn{background:var(--fzred);color:#fff;}
.fzspu-popbtn.ghost{background:#fff;color:#222;border:1px solid var(--fzline);}
CSS; }

function fzspu_js() { return <<<JS
(function(){
  const REST_NONCE = (window.FZSPU && window.FZSPU.restNonce) || '';
  const AJAX_NONCE = (window.FZSPU && window.FZSPU.ajaxNonce) || '';
  const AJAX_URL   = (window.FZSPU && window.FZSPU.ajaxUrl) || '';
  const API = '/wp-json/fzspu/v1';
  const debugEl = document.getElementById('fzspuDebug');
  function dbg(msg){ if(debugEl) debugEl.textContent = msg || ''; }

  
  // WordPress Media Library (front-end)
  function pickFromLibrary(opts){
    return new Promise((resolve,reject)=>{
      try{
        if(!(window.wp && wp.media)) return reject('wp.media not available');
        const frame = wp.media({
          title: (opts && opts.title) ? opts.title : 'انتخاب تصویر',
          button: { text: (opts && opts.buttonText) ? opts.buttonText : 'انتخاب' },
          library: { type: 'image' },
          multiple: !!(opts && opts.multiple)
        });
        frame.on('select', function(){
          const sel = frame.state().get('selection');
          const arr = [];
          sel.each(function(att){
            const j = att.toJSON();
            arr.push({
              id: j.id,
              url: (j.sizes && j.sizes.large && j.sizes.large.url) ? j.sizes.large.url : j.url,
              thumb: (j.sizes && j.sizes.thumbnail && j.sizes.thumbnail.url) ? j.sizes.thumbnail.url : j.url,
              filename: j.filename || ('image-' + j.id + '.jpg')
            });
          });
          resolve(arr);
        });
        frame.open();
      }catch(e){ reject(e); }
    });
  }


  const modal = document.getElementById('fzspuModal');
  const fakeCanvas = document.getElementById('fzspuFakeCanvas');
  const caption = document.getElementById('fzspuCaption');
  function openEditor(label){
    fakeCanvas.textContent = 'پیش‌نمایش (UI) — ' + (label || 'عکس');
    caption.value = '';
    modal.classList.add('show');
  }
  function closeEditor(){ modal.classList.remove('show'); }
  document.getElementById('fzspuModalClose').addEventListener('click', closeEditor);
  document.getElementById('fzspuModalCancel').addEventListener('click', closeEditor);
  document.getElementById('fzspuModalSave').addEventListener('click', function(){ closeEditor(); alert('ذخیره شد (فعلاً فقط UI)'); });

  const success = document.getElementById('fzspuSuccess');
  const viewBtn = document.getElementById('fzspuViewBtn');
  document.getElementById('fzspuCloseSuccess').addEventListener('click', function(){
    success.classList.remove('show');
  });
  function showSuccess(url){
    viewBtn.href = url || '#';
    success.classList.add('show');
  }

  const priceEl = document.getElementById('fzspuPrice');
  function onlyDigits(s){ return (s||'').toString().replace(/[^0-9]/g,''); }
  function formatThousands(d){
    d = onlyDigits(d);
    if(!d) return '';
    return d.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');
  }
  priceEl.addEventListener('input', function(){
    const raw = onlyDigits(priceEl.value);
    priceEl.value = formatThousands(raw);
  });

  function setLine(fillEl, pctEl, val){
    const v = Math.max(0, Math.min(100, val|0));
    fillEl.style.width = v + '%';
    pctEl.textContent = v + '%';
  }

  function xhrUploadRest(file, onProgress){
    return new Promise((resolve, reject)=>{
      const xhr = new XMLHttpRequest();
      xhr.open('POST', API + '/upload', true);
      xhr.setRequestHeader('X-WP-Nonce', REST_NONCE);
      xhr.upload.onprogress = (e)=>{ if(e.lengthComputable) onProgress((e.loaded/e.total)*100); };
      xhr.onreadystatechange = ()=>{
        if(xhr.readyState === 4){
          try{
            const data = JSON.parse(xhr.responseText || '{}');
            if(xhr.status >= 200 && xhr.status < 300 && data && data.ok) resolve(data);
            else reject({status:xhr.status, message:(data && data.message) ? data.message : (xhr.responseText||('HTTP '+xhr.status))});
          }catch(e){ reject({status:xhr.status, message:'parse'}); }
        }
      };
      const fd = new FormData(); fd.append('file', file); xhr.send(fd);
    });
  }

  function xhrUploadAjax(file, onProgress){
    return new Promise((resolve, reject)=>{
      const xhr = new XMLHttpRequest();
      xhr.open('POST', AJAX_URL, true);
      xhr.upload.onprogress = (e)=>{ if(e.lengthComputable) onProgress((e.loaded/e.total)*100); };
      xhr.onreadystatechange = ()=>{
        if(xhr.readyState === 4){
          try{
            const data = JSON.parse(xhr.responseText || '{}');
            if(xhr.status >= 200 && xhr.status < 300 && data && data.ok) resolve(data);
            else reject({status:xhr.status, message:(data && data.message) ? data.message : (xhr.responseText||('HTTP '+xhr.status))});
          }catch(e){ reject({status:xhr.status, message:'parse'}); }
        }
      };
      const fd = new FormData();
      fd.append('action','fzspu_upload');
      fd.append('_ajax_nonce', AJAX_NONCE);
      fd.append('file', file);
      xhr.send(fd);
    });
  }

  async function uploadSmart(file, onProgress){
    try{
      const r = await xhrUploadRest(file, onProgress);
      dbg('REST OK');
      return r;
    }catch(err){
      dbg('REST fail → AJAX');
      return await xhrUploadAjax(file, onProgress);
    }
  }

  async function getCats(){
    try{
      const r = await fetch(API + '/cats', { headers:{'X-WP-Nonce': REST_NONCE} });
      if(!r.ok) throw new Error('rest cats ' + r.status);
      dbg('cats: REST');
      return await r.json();
    }catch(e){
      const fd = new FormData();
      fd.append('action','fzspu_cats');
      fd.append('_ajax_nonce', AJAX_NONCE);
      const r2 = await fetch(AJAX_URL, { method:'POST', body: fd });
      dbg('cats: AJAX');
      return await r2.json();
    }
  }

  async function createProduct(payload){
    try{
      const r = await fetch(API + '/create', {
        method:'POST',
        headers:{'X-WP-Nonce': REST_NONCE, 'Content-Type':'application/json'},
        body: JSON.stringify(payload)
      });
      const d = await r.json();
      if(!r.ok || !d || !d.ok) throw d;
      dbg('create: REST');
      return d;
    }catch(e){
      const fd = new FormData();
      fd.append('action','fzspu_create');
      fd.append('_ajax_nonce', AJAX_NONCE);
      fd.append('payload', JSON.stringify(payload));
      const r2 = await fetch(AJAX_URL, { method:'POST', body: fd });
      const d2 = await r2.json();
      dbg('create: AJAX');
      return d2;
    }
  }

  let featuredId = 0;
  let galleryIds = [];
  let galleryItems = [];
  let submitting = false;

  const featuredInput = document.getElementById('fzspuFeaturedInput');
  const featuredFromLib = document.getElementById('fzspuFeaturedFromLibrary');
  const featuredPreview = document.getElementById('fzspuFeaturedPreview');
  const featuredBox = document.getElementById('fzspuFeaturedBox');
  const submitBtn = document.getElementById('fzspuSubmit');

  document.getElementById('fzspuFeaturedEdit').addEventListener('click', (e)=>{
    e.preventDefault(); e.stopPropagation();
    if (!featuredBox.classList.contains('hasImg')) return;
    openEditor('تصویر شاخص');
  });
  const featuredLine = document.getElementById('fzspuFeaturedLine');
  const featuredFill = featuredLine.querySelector('.fzspu-fill');
  const featuredPct  = featuredLine.querySelector('.fzspu-pct');

  
  if(featuredFromLib){
    featuredFromLib.addEventListener('click', async ()=>{
      try{
        const arr = await pickFromLibrary({title:'انتخاب تصویر شاخص', multiple:false});
        if(!arr || !arr.length) return;
        const a = arr[0];
        featuredId = a.id || 0;
        featuredPreview.src = a.url;
        featuredBox.classList.add('hasImg');
        setLine(featuredFill, featuredPct, 100);
      }catch(e){
        alert('کتابخانه سایت در این صفحه فعال نیست.');
      }
    });
  }

featuredInput.addEventListener('change', async (e)=>{
    const f = e.target.files && e.target.files[0];
    if(!f) return;
    featuredPreview.src = URL.createObjectURL(f);
    featuredBox.classList.add('hasImg');
    setLine(featuredFill, featuredPct, 0);
    try{
      const res = await uploadSmart(f, (pct)=>setLine(featuredFill, featuredPct, pct));
      featuredId = res.attachment_id || 0;
      setLine(featuredFill, featuredPct, 100);
    }catch(err){
      alert('آپلود تصویر شاخص ناموفق: ' + (err.message || err));
      setLine(featuredFill, featuredPct, 0);
      featuredId = 0;
    }
  });

  const galleryInput = document.getElementById('fzspuGalleryInput');
  const galleryFromLib = document.getElementById('fzspuGalleryFromLibrary');
  const thumbs = document.getElementById('fzspuGalleryThumbs');
  function makeThumbWrap(item){
    const wrap = document.createElement('div'); wrap.className='fzspu-thumbWrap';
    const t = document.createElement('div'); t.className='fzspu-thumb';
    const img = document.createElement('img'); img.src = item.url; t.appendChild(img);
    const edit = document.createElement('div'); edit.className='fzspu-thumbEdit'; edit.textContent='ویرایش';
    edit.addEventListener('click',(ev)=>{ ev.preventDefault(); ev.stopPropagation(); openEditor(item.name || 'عکس'); });
    t.appendChild(edit);
    const name = document.createElement('div'); name.className='fzspu-thumbName'; name.textContent=item.name || '';

    const line = document.createElement('div'); line.className='fzspu-thumbLine';
    const bar = document.createElement('div'); bar.className='fzspu-bar';
    const fill = document.createElement('div'); fill.className='fzspu-fill';
    bar.appendChild(fill);
    const pct = document.createElement('div'); pct.className='fzspu-pct'; pct.textContent='0%';
    line.appendChild(bar); line.appendChild(pct);

    wrap.appendChild(t); wrap.appendChild(name); wrap.appendChild(line);
    return {wrap, fill, pct};
  }

  
  if(galleryFromLib){
    galleryFromLib.addEventListener('click', async ()=>{
      try{
        const arr = await pickFromLibrary({title:'انتخاب تصاویر گالری', multiple:true});
        if(!arr || !arr.length) return;
        thumbs.innerHTML=''; galleryIds=[]; galleryItems=[];
        arr.slice(0,12).forEach((a)=>{
          if(a.id) galleryIds.push(a.id);
          galleryItems.push({source:'lib', name: a.filename, url: a.url});
          const ui = makeThumbWrap({name:a.filename, url:a.thumb || a.url});
          thumbs.appendChild(ui.wrap);
          setLine(ui.fill, ui.pct, 100);
        });
        renderDescGallery();
      }catch(e){
        alert('کتابخانه سایت در این صفحه فعال نیست.');
      }
    });
  }

galleryInput.addEventListener('change', async (e)=>{
    thumbs.innerHTML=''; galleryIds=[]; galleryItems=[];
    const files = Array.from(e.target.files||[]).slice(0,12);
    if(!files.length){ return; }

    galleryItems = files.map(f=>({source:'file', name:f.name, url: URL.createObjectURL(f), file:f}));


    for(const item of galleryItems){
      const ui = makeThumbWrap({name:item.name, url:item.url});
      thumbs.appendChild(ui.wrap);
      setLine(ui.fill, ui.pct, 0);
      try{
        const res = await uploadSmart(item.file, (pct)=>setLine(ui.fill, ui.pct, pct));
        const id = res.attachment_id || 0;
        if(id) galleryIds.push(id);
        setLine(ui.fill, ui.pct, 100);
      }catch(err){
        alert('آپلود گالری ناموفق: ' + (err.message || err));
        setLine(ui.fill, ui.pct, 0);
      }
    }
  });

  (async function(){
    const sel = document.getElementById('fzspuCat');
    try{
      const cats = await getCats();
      sel.innerHTML = '<option value="" selected disabled>انتخاب دسته‌بندی</option>';
      (cats||[]).forEach(c=>{
        const opt = document.createElement('option');
        opt.value = c.id; opt.textContent = c.name;
        sel.appendChild(opt);
      });
    }catch(e){
      sel.innerHTML = '<option value="">خطا در بارگذاری</option>';
    }
  })();

  const status = document.getElementById('fzspuStatus');

  function lockSubmit(){
    submitting = true;
    submitBtn.disabled = true;
    submitBtn.textContent = 'در حال ثبت...';
  }
  function unlockSubmit(){
    submitting = false;
    submitBtn.disabled = false;
    submitBtn.textContent = 'ثبت نهایی';
  }
  function doneSubmit(){
    submitting = true;
    submitBtn.disabled = true;
    submitBtn.textContent = 'ثبت شد ✓';
  }

  submitBtn.addEventListener('click', async ()=>{
    if (submitting) return;
    lockSubmit();
    status.textContent = 'در حال ثبت...';

    const title = (document.getElementById('fzspuTitle').value||'').trim();
    const desc  = (document.getElementById('fzspuDesc').value||'').trim();
    const priceRaw = (document.getElementById('fzspuPrice').value||'');
    const price = onlyDigits(priceRaw);
    const catId = (document.getElementById('fzspuCat').value||'').trim();

    if(!featuredId){ status.textContent='تصویر شاخص را آپلود کن.'; unlockSubmit(); return; }
    if(!title){ status.textContent='عنوان را وارد کن.'; unlockSubmit(); return; }
    if(!desc){ status.textContent='توضیحات را وارد کن.'; unlockSubmit(); return; }
    if(!price){ status.textContent='قیمت را وارد کن.'; unlockSubmit(); return; }
    if(!catId){ status.textContent='دسته‌بندی را انتخاب کن.'; unlockSubmit(); return; }

    const payload = { title, description: desc, price, category_id: catId, featured_id: featuredId, gallery_ids: galleryIds };

    try{
      const data = await createProduct(payload);
      if(data && data.ok){
        status.textContent = '';
        doneSubmit();
        showSuccess(data.view_url || '#');
      } else {
        status.textContent = '❌ خطا: ' + (data && data.message ? data.message : 'نامشخص');
        unlockSubmit();
      }
    }catch(e){
      status.textContent = '❌ خطا';
      unlockSubmit();
    }
  });

})();
JS; }

/** REST routes **/
add_action('rest_api_init', function () {
  register_rest_route('fzspu/v1', '/cats', [
    'methods'  => 'GET',
    'permission_callback' => function () { return fzspu_can_use(); },
    'callback' => function () {
      $terms = get_terms(['taxonomy'=>'product_cat','hide_empty'=>false]);
      $out = [];
      foreach ($terms as $t) $out[] = ['id'=>$t->term_id, 'name'=>$t->name];
      return new WP_REST_Response($out, 200);
    }
  ]);

  register_rest_route('fzspu/v1', '/upload', [
    'methods'  => 'POST',
    'permission_callback' => function () { return is_user_logged_in() && current_user_can('upload_files'); },
    'callback' => function () { return fzspu_do_upload(); }
  ]);

  register_rest_route('fzspu/v1', '/create', [
    'methods'  => 'POST',
    'permission_callback' => function () { return fzspu_can_use(); },
    'callback' => function (WP_REST_Request $req) { return fzspu_do_create($req->get_params()); }
  ]);
});

/** AJAX fallbacks **/
add_action('wp_ajax_fzspu_upload', function(){
  check_ajax_referer('fzspu_ajax');
  $r = fzspu_do_upload();
  wp_send_json($r, (!empty($r['ok'])) ? 200 : 400);
});
add_action('wp_ajax_fzspu_cats', function(){
  check_ajax_referer('fzspu_ajax');
  if(!fzspu_can_use()) wp_send_json(['ok'=>false,'message'=>'no permission'], 403);
  $terms = get_terms(['taxonomy'=>'product_cat','hide_empty'=>false]);
  $out = [];
  foreach ($terms as $t) $out[] = ['id'=>$t->term_id, 'name'=>$t->name];
  wp_send_json($out, 200);
});
add_action('wp_ajax_fzspu_create', function(){
  check_ajax_referer('fzspu_ajax');
  if(!fzspu_can_use()) wp_send_json(['ok'=>false,'message'=>'no permission'], 403);
  $payload = isset($_POST['payload']) ? json_decode(stripslashes($_POST['payload']), true) : [];
  $r = fzspu_do_create($payload);
  wp_send_json($r, (!empty($r['ok'])) ? 200 : 400);
});

function fzspu_do_upload(){
  if (empty($_FILES['file'])) return ['ok'=>false,'message'=>'no file'];
  require_once ABSPATH . 'wp-admin/includes/file.php';
  require_once ABSPATH . 'wp-admin/includes/image.php';

  $file = $_FILES['file'];
  $overrides = ['test_form' => false];
  $movefile = wp_handle_upload($file, $overrides);

  if (!$movefile || isset($movefile['error'])) {
    return ['ok'=>false,'message'=> ($movefile['error'] ?? 'upload error')];
  }

  $filename = $movefile['file'];
  $filetype = wp_check_filetype(basename($filename), null);

  $attachment = [
    'post_mime_type' => $filetype['type'],
    'post_title'     => sanitize_file_name(basename($filename)),
    'post_content'   => '',
    'post_status'    => 'inherit'
  ];

  $attach_id = wp_insert_attachment($attachment, $filename);
  $attach_data = wp_generate_attachment_metadata($attach_id, $filename);
  wp_update_attachment_metadata($attach_id, $attach_data);

  return ['ok'=>true,'attachment_id'=>(int)$attach_id,'url'=>wp_get_attachment_url($attach_id)];
}

function fzspu_set_image_seo($attach_id, $base_title, $index = null){
  $attach_id = (int)$attach_id;
  if(!$attach_id) return;

  $suffix = ($index !== null) ? (' – تصویر ' . intval($index)) : '';
  $val = trim($base_title . $suffix);

  update_post_meta($attach_id, '_wp_attachment_image_alt', $val);

  wp_update_post([
    'ID'           => $attach_id,
    'post_title'   => $val,
    'post_excerpt' => $val,
    'post_content' => $val,
  ]);
}

function fzspu_build_gallery_html($gallery_ids, $base_title){
  $gallery_ids = array_values(array_filter(array_map('intval', (array)$gallery_ids)));
  if(!$gallery_ids) return '';

  $html = "\n\n<hr />\n<div class=\"fzspu-desc-gallery\">\n";
  $i = 1;
  foreach($gallery_ids as $aid){
    $url = wp_get_attachment_image_url($aid, 'large');
    if(!$url) continue;
    $alt = esc_attr(trim($base_title . ' – تصویر ' . $i));
    $cap = esc_html(trim($base_title . ' – تصویر ' . $i));
    $html .= "<figure class=\"fzspu-fig\"><img src=\"" . esc_url($url) . "\" alt=\"{$alt}\" loading=\"lazy\" decoding=\"async\" /><figcaption>{$cap}</figcaption></figure>\n";
    $i++;
  }
  $html .= "</div>\n";
  return $html;
}

function fzspu_do_create($params){
  if (!class_exists('WooCommerce')) return ['ok'=>false,'message'=>'WooCommerce not active'];

  $title = isset($params['title']) ? sanitize_text_field($params['title']) : '';
  $description = isset($params['description']) ? wp_kses_post($params['description']) : '';
  $price = isset($params['price']) ? wc_format_decimal($params['price']) : '';
  $cat_id = isset($params['category_id']) ? (int)$params['category_id'] : 0;
  $featured_id = isset($params['featured_id']) ? (int)$params['featured_id'] : 0;
  $gallery_ids = (isset($params['gallery_ids']) && is_array($params['gallery_ids'])) ? $params['gallery_ids'] : [];

  if (!$title || !$description || $price === '' || !$cat_id || !$featured_id) {
    return ['ok'=>false,'message'=>'missing fields'];
  }

  // SEO for images
  fzspu_set_image_seo($featured_id, $title, null);

  $gallery_ids = array_values(array_filter(array_map('intval', $gallery_ids)));
  $idx = 1;
  foreach($gallery_ids as $aid){
    fzspu_set_image_seo($aid, $title, $idx);
    $idx++;
  }

  // Put title inside description as H2 (at top)
  $h2 = '<h2>' . esc_html($title) . '</h2>';
  $full_description = $h2 . "\n" . $description . fzspu_build_gallery_html($gallery_ids, $title);

  $product_id = wp_insert_post([
    'post_type'    => 'product',
    'post_status'  => 'publish',
    'post_title'   => $title,
    'post_content' => $full_description,
  ], true);

  if (is_wp_error($product_id)) return ['ok'=>false,'message'=>$product_id->get_error_message()];

  wp_set_object_terms($product_id, [$cat_id], 'product_cat');
  wp_set_object_terms($product_id, 'simple', 'product_type');

  update_post_meta($product_id, '_regular_price', $price);
  update_post_meta($product_id, '_price', $price);

  set_post_thumbnail($product_id, $featured_id);

  if ($gallery_ids) update_post_meta($product_id, '_product_image_gallery', implode(',', $gallery_ids));

  return [
    'ok'=>true,
    'product_id'=>(int)$product_id,
    'edit_url'=>admin_url('post.php?post=' . $product_id . '&action=edit'),
    'view_url'=>get_permalink($product_id),
  ];
}
همه کد ها
TEXT - 2026-06-05 20:15:54
// نمایش "شماره سفارش | روش پرداخت" در باکس اطلاعات سفارش ادمین add_action('woocommerce_admin_order_data_after_billing_address', function ( $order ) { if ( ! $order ) return; $order_no = $order->get_order_number(); $pay_title = $order->get_payment_method_title(); // نام نمایشی درگاه if ( empty($pay_title) ) { $pay_title = $order->get_payment_method(); // اسلاگ درگاه، اگر نام نبود } echo '<p id="order-quick-info" style="font-weight:600;font-size:15px;direction:rtl;margin-top:8px;"> شماره سفارش: ' . esc_html($order_no) . ( $pay_title ? ' | روش پرداخت: ' . esc_html($pay_title) : '' ) . '</p>'; }, 20 ); /** * Admin order tweaks (WooCommerce): * - Hide only SMS-related lines (safe) * - Enlarge order-item thumbnails column * - Force high-quality image in admin (replace default tiny thumbnail) */ /* ==== CSS: عرض ستون تصویر + ایمنی ستون‌ها + تیتر دلخواه ==== */ add_action('admin_head', function () { ?> <style> /* عرض ستون تصویر آیتم سفارش (در صورت نیاز کمتر/بیشترش کن) */ .woocommerce-page.post-type-shop_order .wc-order-items td.thumb, .woocommerce-page.post-type-shop_order .wc-order-items .wc-order-item-thumbnail, .wc-order-items .thumb, .wc-order-items td.product-thumbnail, .wc-order-item-thumbnail{ width: 160px !important; min-width: 160px !important; max-width: none !important; } .woocommerce-page.post-type-shop_order .wc-order-items td.thumb img, .woocommerce-page.post-type-shop_order .wc-order-items .wc-order-item-thumbnail img, .wc-order-items .thumb img, .wc-order-item-thumbnail img{ display: block !important; width: 100% !important; height: auto !important; max-width: none !important; object-fit: contain !important; image-rendering: auto; } /* مطمئن شو ستون‌های اطلاعات مشتری پنهان نشوند */ .order_data_column{ display:block !important; } /* (اختیاری) ریز کردن تیتر سفارشی */ #order-quick-info{ font-size:13px !important; font-weight:600; } </style> <?php }); /* ==== JS: فقط خطوط مربوط به «پیامک/SMS» را مخفی کن (نه والدهای بزرگ) ==== */ add_action('admin_footer', function () { ?> <script> (function(){ var inOrder = document.querySelector('.woocommerce-order-data, .wc-order-items'); if(!inOrder) return; var TEXTS = [ 'آیا مشتری مایل به دریافت پیامک هست', 'مشتری حق انتخاب وضعیت های دریافت پیامک را ندارد', 'دریافت پیامک','SMS','sms' ]; // اگر با لیبل مشخص است، همان فیلد را مخفی کن var label = document.querySelector('label[for="_billing_sms_consent"], label[for="billing_sms_consent"]'); if (label) { var field = label.closest('.form-field, .options_group, p'); if (field) field.style.display = 'none'; } function hideSmsLines(ctx){ (ctx||document).querySelectorAll('#order_data p, .order_data_column p, .postbox .inside p, .options_group .form-field, .woocommerce-order-data p') .forEach(function(el){ var t = (el.innerText||'').replace(/\s+/g,' ').trim(); if (!t) return; if (TEXTS.some(function(x){ return t.indexOf(x)!==-1; })) el.style.display='none'; }); document.querySelectorAll('.order_data_column').forEach(function(col){ col.style.removeProperty('display'); col.hidden=false; }); } hideSmsLines(document); new MutationObserver(function(){ hideSmsLines(document); }).observe(document.body,{childList:true,subtree:true}); })(); </script> <?php }); /* ==== کیفیت بالا: جایگزینی تامب‌نیل کوچک با تصویر بزرگ/اصلی ==== */ /* این فیلتر، HTML تصویر آیتم را با سایز بزرگ‌تر بازتولید می‌کند. */ add_filter('woocommerce_admin_order_item_thumbnail', function($thumbnail, $item_id, $item){ if ( ! is_admin() ) return $thumbnail; if ( ! $item || ! is_a($item, 'WC_Order_Item_Product') ) return $thumbnail; $product = $item->get_product(); if ( ! $product ) return $thumbnail; $image_id = $product->get_image_id(); if ( ! $image_id ) return $thumbnail; // 'large' معمولاً کافی و سبک است. اگر نهایت کیفیت می‌خواهی 'full' بگذار. $size = 'large'; // یا: 'woocommerce_single' / 'full' $html = wp_get_attachment_image($image_id, $size, false, array( 'loading' => 'eager', 'decoding'=> 'async', 'style' => 'width:100%;height:auto;max-width:none;display:block' )); return $html ?: $thumbnail; }, 10, 3); /* (اختیاری) اگر نسخه‌های قدیمی از این فیلتر استفاده کنند، سایز را هم بزرگ‌تر اعلام کن */ add_filter('woocommerce_admin_order_item_thumbnail_size', function(){ return 'large'; // در صورت نیاز 'full' یا 'woocommerce_single' }); // اجرای JS فقط در ادمین برای تاگل مبلغ ⇄ "تسویه‌شده ✅" add_action('admin_footer', function () { ?> <style> /* استایل حالت تسویه‌شده */ .fz-paid-wrapper{display:inline-flex;flex-direction:column;align-items:flex-start;gap:4px;} .fz-paid-label{color:#16a34a;font-weight:800;font-size:18px;line-height:1.2;} .fz-paid-tick{color:#16a34a;font-size:22px;line-height:1;} .fz-amount{cursor:pointer;} </style> <script> (function(){ function ready(fn){ if(document.readyState!=='loading') fn(); else document.addEventListener('DOMContentLoaded',fn); } ready(function(){ // فقط صفحه ویرایش سفارش ووکامرس var isOrderEdit = document.body.classList.contains('post-type-shop_order') || document.querySelector('.woocommerce-order-data, .wc-order-items'); if(!isOrderEdit) return; // تشخیص المان مبلغ function isAmount(el){ return el && el.classList && (el.classList.contains('amount') || el.classList.contains('woocommerce-Price-amount')); } // برای کلیک‌پذیر شدن و ذخیره متن اصلی function prime(ctx){ (ctx||document).querySelectorAll( '.wc-order-items .amount, .wc-order-totals .amount,'+ '.wc-order-items .woocommerce-Price-amount, .wc-order-totals .woocommerce-Price-amount' ).forEach(function(el){ if (!el.dataset.fzOriginal) el.dataset.fzOriginal = el.innerHTML; el.classList.add('fz-amount'); el.style.cursor='pointer'; }); } // رفت و برگشت بین مبلغ و "تسویه‌شده" function toggle(el){ if(!el.closest('.wc-order-items, .wc-order-totals')) return; // فقط جدول آیتم‌ها/جمع‌کل if(el.classList.contains('fz-paid')){ el.innerHTML = el.dataset.fzOriginal || el.innerHTML; el.classList.remove('fz-paid'); }else{ el.innerHTML = '<span class="fz-paid-wrapper">'+ '<span class="fz-paid-label">تسویه\u200cشده</span>'+ '<span class="fz-paid-tick">✅</span>'+ '</span>'; el.classList.add('fz-paid'); } } // Event Delegation تا با Ajax/HPOS هم کار کند document.addEventListener('click', function(e){ var el = e.target.closest('.amount, .woocommerce-Price-amount'); if(!el) return; if(!el.closest('.wc-order-items, .wc-order-totals')) return; e.preventDefault(); toggle(el); }, true); // آماده‌سازی اولیه و پس از تغییرات Ajax prime(document); new MutationObserver(function(m){ m.forEach(function(mu){ (mu.addedNodes||[]).forEach(function(n){ if(n.nodeType!==1) return; if(n.matches && (n.matches('.amount')||n.matches('.woocommerce-Price-amount'))) prime(n); else prime(n); }); }); }).observe(document.body,{childList:true,subtree:true}); }); })(); </script> <?php }); /* مخفی کردن همه دکمه‌های خرید و بیعانه در کارت محصولات (فروشگاه و دسته‌ها) */ .archive.woocommerce .product-small a.button, .archive.woocommerce .product-small button.button, .archive.woocommerce .product-small .yith-wcdp { display: none !important; } /************* * آماده تحویل – متاباکس + نمایش در محصول و لیست *************/ /*----------------------------- متاباکس در صفحه محصول -----------------------------*/ add_action( 'add_meta_boxes', 'fzd_ready_add_metabox' ); function fzd_ready_add_metabox() { add_meta_box( 'fzd_ready_box', 'آماده تحویل', 'fzd_ready_metabox_callback', 'product', 'side', 'high' ); } function fzd_ready_metabox_callback( $post ) { $rows = get_post_meta( $post->ID, '_fzd_ready_rows', true ); if ( ! is_array( $rows ) || empty( $rows ) ) { // فقط یک ردیف خالی، بدون مقدار پیش‌فرض $rows = array( array( 'color' => '', 'days' => '' ), ); } $note = get_post_meta( $post->ID, '_fzd_ready_note', true ); wp_nonce_field( 'fzd_ready_save', 'fzd_ready_nonce' ); echo '<p>برای هر رنگ آماده تحویل، یک ردیف وارد کن.</p>'; echo '<div id="fzd-ready-rows">'; foreach ( $rows as $row ) { $color = isset( $row['color'] ) ? $row['color'] : ''; $days = isset( $row['days'] ) ? (int) $row['days'] : 3; echo '<div class="fzd-ready-row" style="margin-bottom:6px;border-bottom:1px solid #ddd;padding-bottom:6px;">'; echo '<input type="text" name="fzd_ready_color[]" value="' . esc_attr( $color ) . '" placeholder="رنگ (مثلاً خودرنگ)" style="width:100%;margin-bottom:4px;">'; echo '<input type="number" name="fzd_ready_days[]" value="' . esc_attr( $days ) . '" min="0" max="365" style="width:100%;margin-bottom:4px;" placeholder="روز تحویل">'; echo '<button type="button" class="button fzd-ready-remove">حذف</button>'; echo '</div>'; } echo '</div>'; echo '<button type="button" class="button button-secondary" id="fzd-ready-add">+ افزودن رنگ دیگر</button>'; echo '<hr><p><strong>توضیح اضافه (اختیاری):</strong><br><small>این متن به رنگ سبز، زیر توضیحات آماده تحویل در صفحه محصول و دسته‌بندی نمایش داده می‌شود.</small></p>'; echo '<textarea name="fzd_ready_note" style="width:100%;min-height:70px;">' . esc_textarea( $note ) . '</textarea>'; ?> <script> (function($){ $(function(){ var $wrap = $('#fzd-ready-rows'); $('#fzd-ready-add').on('click', function(e){ e.preventDefault(); var $first = $wrap.find('.fzd-ready-row:first').clone(); $first.find('input').val(''); $wrap.append($first); }); $wrap.on('click', '.fzd-ready-remove', function(e){ e.preventDefault(); if ($wrap.find('.fzd-ready-row').length > 1) { $(this).closest('.fzd-ready-row').remove(); } else { $(this).closest('.fzd-ready-row').find('input').val(''); } }); }); })(jQuery); </script> <?php } add_action( 'save_post_product', 'fzd_ready_save_metabox' ); function fzd_ready_save_metabox( $post_id ) { if ( ! isset( $_POST['fzd_ready_nonce'] ) || ! wp_verify_nonce( $_POST['fzd_ready_nonce'], 'fzd_ready_save' ) ) return; if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return; // رنگ‌ها + روزها if ( isset( $_POST['fzd_ready_color'], $_POST['fzd_ready_days'] ) ) { $colors = (array) $_POST['fzd_ready_color']; $days = (array) $_POST['fzd_ready_days']; $rows = array(); foreach ( $colors as $i => $color ) { $color = sanitize_text_field( wp_unslash( $color ) ); $d = isset( $days[ $i ] ) ? (int) $days[ $i ] : 0; if ( $color === '' ) continue; if ( $d < 1 ) $d = 1; $rows[] = array( 'color' => $color, 'days' => $d, ); } if ( ! empty( $rows ) ) { update_post_meta( $post_id, '_fzd_ready_rows', $rows ); } else { delete_post_meta( $post_id, '_fzd_ready_rows' ); } }// توضیح دستی if ( isset( $_POST['fzd_ready_note'] ) ) { $note = sanitize_textarea_field( wp_unslash( $_POST['fzd_ready_note'] ) ); if ( $note !== '' ) { update_post_meta( $post_id, '_fzd_ready_note', $note ); } else { delete_post_meta( $post_id, '_fzd_ready_note' ); } } } /*----------------------------- توابع کمکی (شمسی + ارقام فارسی) -----------------------------*/ function fzd_ready_get_rows( $product_id ) { $rows = get_post_meta( $product_id, '_fzd_ready_rows', true ); return is_array( $rows ) ? $rows : array(); } function fzd_ready_get_note( $product_id ) { $note = get_post_meta( $product_id, '_fzd_ready_note', true ); return trim( (string) $note ); } function fzd_ready_persian_digits( $str ) { $en = array('0','1','2','3','4','5','6','7','8','9'); $fa = array('۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'); return str_replace( $en, $fa, (string) $str ); } function fzd_ready_convert_jalali( $timestamp ) { $gy = (int) gmdate( 'Y', $timestamp ); $gm = (int) gmdate( 'n', $timestamp ); $gd = (int) gmdate( 'j', $timestamp ); $g_d_m = array(0,31,59,90,120,151,181,212,243,273,304,334); if ( $gy > 1600 ) { $jy = 979; $gy -= 1600; } else { $jy = 0; $gy -= 621; } $gy2 = ( $gm > 2 ) ? ( $gy + 1 ) : $gy; $days = 365*$gy + (int)(($gy2+3)/4) - (int)(($gy2+99)/100) + (int)(($gy2+399)/400) - 80 + $gd + $g_d_m[$gm-1]; $jy += 33*(int)($days/12053); $days %= 12053; $jy += 4*(int)($days/1461); $days %= 1461; if ( $days > 365 ) { $jy += (int)(($days-1)/365); $days = ($days-1)%365; } if ( $days < 186 ) { $jm = 1 + (int)($days/31); $jd = 1 + ($days%31); } else { $jm = 7 + (int)(($days-186)/30); $jd = 1 + (($days-186)%30); } $months = array( 1=>'فروردین',2=>'اردیبهشت',3=>'خرداد',4=>'تیر',5=>'مرداد',6=>'شهریور', 7=>'مهر',8=>'آبان',9=>'آذر',10=>'دی',11=>'بهمن',12=>'اسفند', ); $day_fa = fzd_ready_persian_digits( $jd ); $month_fa = isset( $months[$jm] ) ? $months[$jm] : ''; return $day_fa . ' ' . $month_fa; } /*----------------------------- نمایش داخل صفحه محصول -----------------------------*/ add_action( 'woocommerce_single_product_summary', 'fzd_ready_single_box', 12 ); function fzd_ready_single_box() { if ( ! is_product() ) return; $product_id = get_the_ID(); if ( ! $product_id ) return; $product = wc_get_product( $product_id ); if ( ! $product || ! $product->is_in_stock() ) return; $rows = fzd_ready_get_rows( $product_id ); if ( empty( $rows ) ) return; echo '<div style="margin-top:10px;margin-bottom:10px;padding:8px 12px;border:1px solid #e53935;border-radius:6px;font-size:16px;line-height:1.9;">'; echo '<strong>رنگ‌های آماده تحویل:</strong>'; echo '<ul style="margin:5px 0 0 0;padding-right:18px;list-style:disc;">'; foreach ( $rows as $row ) { $color = isset( $row['color'] ) ? $row['color'] : ''; $days = isset( $row['days'] ) ? (int) $row['days'] : 1; if ( $color === '' ) continue; if ( $days < 1 ) $days = 1; $ts = current_time( 'timestamp' ) + $days * DAY_IN_SECONDS; $date = fzd_ready_convert_jalali( $ts ); $days_fa = fzd_ready_persian_digits( $days ); echo '<li>این محصول را در رنگ <strong>' . esc_html( $color ) . '</strong> تا <strong>' . esc_html( $date ) . '</strong> تحویل بگیرید (حدود ' . $days_fa . ' روزه).</li>'; } echo '</ul>'; // متن پیش‌فرض – مشکی $default_note = 'سایر رنگ‌ها به صورت سفارشی تولید می‌شوند و زمان تحویل آن‌ها کمی بیشتر است؛ پس از ثبت سفارش، زمان دقیق با شما هماهنگ می‌شود.'; echo '<p style="margin-top:8px;font-size:14px;color:#333333;">' . esc_html( $default_note ) . '</p>'; // توضیح دستی – سبز $note = fzd_ready_get_note( $product_id ); if ( $note !== '' ) { echo '<p style="margin-top:2px;font-size:16px;color:#388e3c;">' . esc_html( $note ) . '</p>'; } echo '</div>'; }/*----------------------------- لیبل «آماده تحویل» روی عکس (فلت‌سام) -----------------------------*/ add_action( 'flatsome_woocommerce_shop_loop_images', 'fzd_ready_badge', 20 ); function fzd_ready_badge() { global $product; if ( ! $product || ! is_a( $product, 'WC_Product' ) ) return; if ( ! $product->is_in_stock() ) return; $rows = fzd_ready_get_rows( $product->get_id() ); if ( empty( $rows ) ) return; // استایل فقط یک بار چاپ شود static $printed = false; if ( ! $printed ) { echo '<style> .product-small .box-image { position: relative; } /* لیبل آماده تحویل – گوشه بالا راست */ .product-small .box-image .fzd-ready-badge { position: absolute; top: -2px; right: 8px; z-index: 10; } /* بادج تخفیف فلت‌سام – بیاد گوشه بالا چپ */ .product-small .box-image .badge-container { left: 8px; right: auto; } </style>'; $printed = true; } echo '<span class="fzd-ready-badge" style="display:inline-block;background:#e53935;color:#ffffff;padding:3px 10px;border-radius:16px;font-size:14px;">آماده تحویل</span>'; } /*----------------------------- متن تحویل + توضیح سبز در لیست محصولات -----------------------------*/ add_action( 'woocommerce_after_shop_loop_item_title', 'fzd_ready_loop_text', 15 ); function fzd_ready_loop_text() { global $product; if ( ! $product || ! is_a( $product, 'WC_Product' ) ) return; if ( ! $product->is_in_stock() ) return; $rows = fzd_ready_get_rows( $product->get_id() ); if ( empty( $rows ) ) return; $max = 2; // حداکثر دو رنگ در دسته‌بندی $count = 0; // متن قرمز زیر محصول echo '<div style="margin-top:4px;font-size:14px;color:#c62828;line-height:1.7;">'; foreach ( $rows as $row ) { if ( $count >= $max ) break; $color = isset( $row['color'] ) ? $row['color'] : ''; $days = isset( $row['days'] ) ? (int) $row['days'] : 1; if ( $color === '' ) continue; if ( $days < 1 ) $days = 1; $ts = current_time( 'timestamp' ) + $days * DAY_IN_SECONDS; $date = fzd_ready_convert_jalali( $ts ); echo 'رنگ ' . esc_html( $color ) . ' را تا ' . esc_html( $date ) . ' تحویل بگیرید<br>'; $count++; } echo '</div>'; // توضیح دستی سبز $note = fzd_ready_get_note( $product->get_id() ); if ( $note !== '' ) { echo '<div style="margin-top:2px;font-size:14px;color:#388e3c;line-height:1.6;">' . esc_html( $note ) . '</div>'; } } /* ---------------------------------------------------- * 1) فیلد «تعداد در تخفیف» برای محصول ساده * --------------------------------------------------*/ add_action( 'woocommerce_product_options_pricing', 'my_add_promo_limit_field_simple' ); function my_add_promo_limit_field_simple() { woocommerce_wp_text_input( array( 'id' => '_promo_limit', 'label' => 'تعداد در تخفیف', 'type' => 'number', 'desc_tip' => true, 'description' => 'تعداد کل آیتم‌هایی که با قیمت حراج فروخته می‌شوند (در کل). اگر خالی یا 0 باشد، محدودیتی اعمال نمی‌شود.', 'custom_attributes' => array( 'min' => '0', 'step' => '1', ), ) ); } add_action( 'woocommerce_admin_process_product_object', 'my_save_promo_limit_field_simple' ); function my_save_promo_limit_field_simple( $product ) { if ( isset( $_POST['_promo_limit'] ) ) { $promo_limit = max( 0, intval( $_POST['_promo_limit'] ) ); $product->update_meta_data( '_promo_limit', $promo_limit ); } } /* ---------------------------------------------------- * 2) فیلد «تعداد در تخفیف» برای هر ورییشن * --------------------------------------------------*/ add_action( 'woocommerce_product_after_variable_attributes', 'my_add_variation_promo_limit_field', 10, 3 ); function my_add_variation_promo_limit_field( $loop, $variation_data, $variation ) { woocommerce_wp_text_input( array( 'id' => "variable_promo_limit_{$loop}", 'name' => "variable_promo_limit[{$loop}]", 'value' => get_post_meta( $variation->ID, '_promo_limit', true ), 'label' => 'تعداد در تخفیف', 'type' => 'number', 'desc_tip' => true, 'description' => 'تعداد کل این ورییشن که با قیمت حراج فروخته می‌شود (در کل).', 'custom_attributes' => array( 'min' => '0', 'step' => '1', ), ) ); } add_action( 'woocommerce_save_product_variation', 'my_save_variation_promo_limit_field', 10, 2 ); function my_save_variation_promo_limit_field( $variation_id, $i ) { if ( isset( $_POST['variable_promo_limit'][ $i ] ) ) { $promo_limit = max( 0, intval( $_POST['variable_promo_limit'][ $i ] ) ); update_post_meta( $variation_id, '_promo_limit', $promo_limit ); } } /* ---------------------------------------------------- * کمک‌تابع: متن توضیح تخفیف برای یک محصول/ورییشن * --------------------------------------------------*/ function my_get_promo_message_for_product( $pid ) { $promo_limit = intval( get_post_meta( $pid, '_promo_limit', true ) ); if ( $promo_limit <= 0 ) { return ''; } $sold_so_far = intval( get_option( 'promo_sold_' . $pid, 0 ) ); $remaining = max( 0, $promo_limit - $sold_so_far ); if ( $remaining <= 0 ) { return 'تخفیف این محصول به پایان رسیده و از این پس با قیمت عادی و زمان تحویل پیش‌فرض ارسال می‌شود.'; } if ( $remaining == 1 ) { return 'از این محصول با این تخفیف فقط ۱ عدد دیگر موجود است؛ تعداد بیشتر با قیمت عادی و زمان تحویل پیش‌فرض ارسال می‌شود.'; } return 'از این محصول با این تخفیف فقط ' . $remaining . ' عدد دیگر موجود است؛ تعداد بیشتر با قیمت عادی و زمان تحویل پیش‌فرض ارسال می‌شود.'; } /* ---------------------------------------------------- * 3) اعمال تخفیف در سبد بر اساس محدودیت کلی هر محصول/ورییشن * --------------------------------------------------*/ add_action( 'woocommerce_before_calculate_totals', 'my_limit_discount_per_product_or_variation', 20, 1 ); function my_limit_discount_per_product_or_variation( $cart ) { if ( is_admin() && ! defined( 'DOING_AJAX' ) ) return; if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 ) return; if ( $cart->is_empty() ) return; // گروه‌بندی آیتم‌ها بر اساس ID محصول/ورییشن $items_by_pid = array(); foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) { $product = $cart_item['data']; if ( ! $product ) continue;$pid = $product->get_id(); // برای ورییشن، ID خود ورییشن if ( ! isset( $items_by_pid[ $pid ] ) ) { $items_by_pid[ $pid ] = array(); } $items_by_pid[ $pid ][] = $cart_item_key; } // اعمال تخفیف برای هر محصول/ورییشن با سقف کلی foreach ( $items_by_pid as $pid => $cart_item_keys ) { $promo_limit = intval( get_post_meta( $pid, '_promo_limit', true ) ); if ( $promo_limit <= 0 ) { continue; // محدودیتی تعریف نشده } $sold_so_far = intval( get_option( 'promo_sold_' . $pid, 0 ) ); $remaining = max( 0, $promo_limit - $sold_so_far ); // اگر چیزی باقی نمانده، همه با قیمت عادی if ( $remaining <= 0 ) { foreach ( $cart_item_keys as $cart_item_key ) { $cart_items = $cart->get_cart(); if ( ! isset( $cart_items[ $cart_item_key ] ) ) continue; $cart_item = $cart_items[ $cart_item_key ]; $product = $cart_item['data']; $regular = (float) $product->get_regular_price(); if ( $regular ) { $product->set_price( $regular ); } } continue; } // روی تک‌تک خطوط برای این محصول/ورییشن foreach ( $cart_item_keys as $cart_item_key ) { $cart_items = $cart->get_cart(); if ( ! isset( $cart_items[ $cart_item_key ] ) ) continue; $cart_item = $cart_items[ $cart_item_key ]; $product = $cart_item['data']; $qty = $cart_item['quantity']; $regular_price = (float) $product->get_regular_price(); $sale_price = (float) $product->get_sale_price(); if ( ! $regular_price || ! $sale_price ) { continue; // قیمت حراج تنظیم نشده } if ( $remaining <= 0 ) { $product->set_price( $regular_price ); continue; } // تعداد تخفیف‌دار در این خط $discount_qty = min( $remaining, $qty ); // جمع این خط: discount_qty با حراج، بقیه با قیمت عادی $line_total = $discount_qty * $sale_price + ( $qty - $discount_qty ) * $regular_price; // قیمت واحد میانگین، تا جمع درست دربیاید $product->set_price( $line_total / $qty ); // از سقف باقی‌مانده کم کن $remaining -= $discount_qty; } } } /* ---------------------------------------------------- * 4) به‌روزرسانی تعداد فروخته‌شده‌ی تخفیفی بعد از تغییر وضعیت سفارش * (processing / completed / on-hold) * --------------------------------------------------*/ add_action( 'woocommerce_order_status_changed', 'my_update_promo_sold_qty_for_items', 10, 4 ); function my_update_promo_sold_qty_for_items( $order_id, $old_status, $new_status, $order ) { // فقط وقتی سفارش می‌ره روی وضعیت‌های مهم $target_statuses = array( 'processing', 'completed', 'on-hold' ); if ( ! in_array( $new_status, $target_statuses, true ) ) { return; } // نذاریم یک سفارش دوبار حساب شود if ( 'yes' === get_post_meta( $order_id, '_promo_sold_counted', true ) ) { return; } update_post_meta( $order_id, '_promo_sold_counted', 'yes' ); if ( ! $order || ! is_a( $order, 'WC_Order' ) ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } } foreach ( $order->get_items() as $item ) { $product = $item->get_product(); if ( ! $product ) continue; $pid = $product->get_id(); $promo_limit = intval( get_post_meta( $pid, '_promo_limit', true ) ); if ( $promo_limit <= 0 ) { continue; // برای این آیتم محدودیت تعریف نشده } $sold_so_far = intval( get_option( 'promo_sold_' . $pid, 0 ) ); $remaining = max( 0, $promo_limit - $sold_so_far ); if ( $remaining <= 0 ) { continue; }$qty = $item->get_quantity(); $discount_qty = min( $remaining, $qty ); // فقط همین مقدار را به‌عنوان تخفیفی ثبت می‌کنیم $sold_so_far += $discount_qty; update_option( 'promo_sold_' . $pid, $sold_so_far ); // اگر سقف پر شد، قیمت حراج را از خود محصول/ورییشن بردار if ( $sold_so_far >= $promo_limit ) { $regular = get_post_meta( $pid, '_regular_price', true ); update_post_meta( $pid, '_sale_price', '' ); update_post_meta( $pid, '_price', $regular ); } } } /* ---------------------------------------------------- * 5) نمایش پیام زیر اسم محصول در سبد خرید * --------------------------------------------------*/ add_filter( 'woocommerce_cart_item_name', 'my_show_promo_limit_message_cart', 10, 3 ); function my_show_promo_limit_message_cart( $product_name, $cart_item, $cart_item_key ) { $product = isset( $cart_item['data'] ) ? $cart_item['data'] : false; if ( ! $product ) { return $product_name; } $pid = $product->get_id(); $promo_limit = intval( get_post_meta( $pid, '_promo_limit', true ) ); if ( $promo_limit <= 0 ) { return $product_name; // محدودیت تعریف نشده } if ( ! function_exists( 'WC' ) || ! WC()->cart ) { return $product_name; } $sold_so_far = intval( get_option( 'promo_sold_' . $pid, 0 ) ); $remaining = max( 0, $promo_limit - $sold_so_far ); $cart = WC()->cart; $total_qty = 0; // مجموع تعداد این محصول/ورییشن در کل سبد foreach ( $cart->get_cart() as $ci ) { $p = isset( $ci['data'] ) ? $ci['data'] : false; if ( ! $p ) continue; if ( $p->get_id() == $pid ) { $total_qty += $ci['quantity']; } } if ( $total_qty <= 0 ) { return $product_name; } $discountable_in_cart = min( $remaining, $total_qty ); if ( $remaining <= 0 ) { $msg = 'تخفیف این محصول تمام شده و همهٔ تعداد با قیمت عادی محاسبه می‌شوند.'; } elseif ( $total_qty <= $discountable_in_cart ) { $msg = 'تا ' . $discountable_in_cart . ' عدد از این محصول با قیمت تخفیف محاسبه می‌شود.'; } else { $nondiscounted = $total_qty - $discountable_in_cart; $msg = 'در این سبد فقط ' . $discountable_in_cart . ' عدد از این محصول با قیمت تخفیف محاسبه می‌شود و ' . $nondiscounted . ' عدد بعدی با قیمت عادی و زمان تحویل پیش‌فرض هستند.'; } return $product_name . '<div class="promo-limit-msg" style="font-size:12px; color:#d33; margin-top:3px;">' . esc_html( $msg ) . '</div>'; } /* ---------------------------------------------------- * 6) نمایش پیام روی صفحه محصول * - ساده: زیر قیمت * - متغیر: کنار متن موجودی ورییشن * --------------------------------------------------*/ // محصول ساده: نمایش زیر قیمت add_action( 'woocommerce_single_product_summary', 'my_show_promo_msg_on_single_simple', 11 ); function my_show_promo_msg_on_single_simple() { global $product; if ( ! $product ) return; if ( $product->is_type( 'simple' ) ) { $msg = my_get_promo_message_for_product( $product->get_id() ); if ( $msg ) { echo '<div class="promo-msg-single" style="font-size:13px; color:#d33; margin-top:5px;">' . esc_html( $msg ) . '</div>'; } } } // محصول متغیر: اضافه کردن پیام به availability_html هر ورییشن add_filter( 'woocommerce_available_variation', 'my_add_promo_msg_to_variation_data', 10, 3 ); function my_add_promo_msg_to_variation_data( $data, $product, $variation ) { $msg = my_get_promo_message_for_product( $variation->get_id() ); if ( $msg ) { if ( ! empty( $data['availability_html'] ) ) { $data['availability_html'] .= '<br><span class="promo-msg-single" style="color:#d33; font-size:13px;">' . esc_html( $msg ) . '</span>'; } else { $data['availability_html'] = '<p class="stock promo-msg-single" style="color:#d33; font-size:13px;">' . esc_html( $msg ) . '</p>'; } } return $data; } /* نمایش تعداد باقی‌مانده در تخفیف روی صفحه محصول / ورییشن در ادمین */ /* محصول ساده: زیر فیلد تعداد در تخفیف */ add_action( 'woocommerce_product_options_pricing', function () { global $post; if ( ! $post ) return; $product_id = $post->ID; $promo_limit = intval( get_post_meta( $product_id, '_promo_limit', true ) ); if ( $promo_limit <= 0 ) return; $sold_so_far = intval( get_option( 'promo_sold_' . $product_id, 0 ) ); $remaining = max( 0, $promo_limit - $sold_so_far ); echo '<p style="margin-top:-8px; color:#0073aa; font-size:12px;">' . 'باقی‌مانده در تخفیف: <strong>' . $remaining . '</strong> عدد' . '</p>'; } ); /* محصول متغیر: برای هر ورییشن کنار فیلد تعداد در تخفیف */ add_action( 'woocommerce_product_after_variable_attributes', function( $loop, $variation_data, $variation ) { $vid = $variation->ID; // ID خود ورییشن $promo_limit = intval( get_post_meta( $vid, '_promo_limit', true ) ); if ( $promo_limit <= 0 ) return; $sold_so_far = intval( get_option( 'promo_sold_' . $vid, 0 ) ); $remaining = max( 0, $promo_limit - $sold_so_far ); echo '<p style="margin:3px 0 0; color:#0073aa; font-size:12px;">' . 'باقی‌مانده در تخفیف برای این تنوع: <strong>' . $remaining . '</strong> عدد' . '</p>'; }, 20, 3 ); /* ========== Default sort for Shop + Categories = Best sellers (last 90 days) Label shown to user = "پرفروش‌ترین‌ها" Also calculates _sales_90d daily + manual recalc link ========== */ define('FARYAZAN_SALES_META', '_sales_90d'); define('FARYAZAN_ORDERBY_KEY', 'sales_90d'); /* 1) Schedule daily update */ add_action('init', function () { if (!wp_next_scheduled('faryazan_update_90d_metrics')) { wp_schedule_event(time() + 300, 'daily', 'faryazan_update_90d_metrics'); } }); /* 2) Calculate sales in last 90 days and store in _sales_90d */ add_action('faryazan_update_90d_metrics', function () { if (!function_exists('wc_get_orders') || !function_exists('wc_get_products')) return; $after_ts = time() - (90 * DAY_IN_SECONDS); $after = gmdate('Y-m-d H:i:s', $after_ts); $sales = []; $page = 1; $per_page = 100; do { $orders = wc_get_orders([ 'status' => ['processing', 'completed'], 'limit' => $per_page, 'paged' => $page, 'date_created' => '>' . $after, 'return' => 'objects', ]); foreach ($orders as $order) { foreach ($order->get_items('line_item') as $item) { $pid = (int) $item->get_product_id(); if ($pid <= 0) continue; $qty = (int) $item->get_quantity(); if ($qty <= 0) continue; $sales[$pid] = ($sales[$pid] ?? 0) + $qty; } } $page++; } while (!empty($orders)); $product_ids = wc_get_products(['return' => 'ids', 'limit' => -1]); foreach ($product_ids as $pid) { $s90 = (int) ($sales[$pid] ?? 0); update_post_meta($pid, FARYAZAN_SALES_META, $s90); } }); /* 3) Manual recalculation (admin only): https://YOURDOMAIN.COM/?faryazan_recalc=1 */ add_action('init', function () { if (!is_user_logged_in() || !current_user_can('manage_woocommerce')) return; if (isset($_GET['faryazan_recalc']) && $_GET['faryazan_recalc'] === '1') { do_action('faryazan_update_90d_metrics'); wp_die('OK ✅ 90-day sales recalculated. You can close this page.'); } }); /* 4) Rename the option shown to user -> "پرفروش‌ترین‌ها" */ add_filter('woocommerce_catalog_orderby', function ($options) { $options[FARYAZAN_ORDERBY_KEY] = 'پرفروش‌ترین‌ها'; return $options; }, 20); add_filter('woocommerce_default_catalog_orderby_options', function ($options) { $options[FARYAZAN_ORDERBY_KEY] = 'پرفروش‌ترین‌ها'; return $options; }, 20); /* 5) Make our option the DEFAULT for all shop/category pages */ add_filter('woocommerce_default_catalog_orderby', function ($default) { return FARYAZAN_ORDERBY_KEY; }, 20); /* 6) Apply ordering ONLY when that option is used (which is now default too) */ add_filter('woocommerce_get_catalog_ordering_args', function ($args, $orderby, $order) { if ($orderby === FARYAZAN_ORDERBY_KEY) { $args['orderby'] = 'meta_value_num'; $args['order'] = 'DESC'; $args['meta_key'] = FARYAZAN_SALES_META; // Keep products visible even if meta missing $args['meta_query'] = [ 'relation' => 'OR', [ 'key' => FARYAZAN_SALES_META, 'compare' => 'EXISTS', 'type' => 'NUMERIC', ], [ 'key' => FARYAZAN_SALES_META, 'compare' => 'NOT EXISTS', ], ]; } return $args; }, 20, 3); // جستجو در محصولات ووکامرس بر اساس SKU (سازگار با AJAX و فلت‌سام) add_filter( 'posts_join', 'adel_search_join_sku', 10, 2 ); function adel_search_join_sku( $join, $query ) { global $wpdb; // توی ادمین معمولی کاری نکن، ولی اجازه بده توی AJAX اجرا بشه if ( is_admin() && ( ! function_exists('wp_doing_ajax') || ! wp_doing_ajax() ) ) { return $join; } // فقط روی کوئری‌هایی که برای product هستن $post_types = (array) $query->get( 'post_type' ); if ( ! in_array( 'product', $post_types ) && ! empty( $post_types ) ) { return $join; } // فقط وقتی رشته جستجو وجود داره $search_term = $query->get( 's' ); if ( empty( $search_term ) ) { return $join; } // جوین کردن متای _sku $join .= " LEFT JOIN {$wpdb->postmeta} AS sku_pm ON ({$wpdb->posts}.ID = sku_pm.post_id AND sku_pm.meta_key = '_sku') "; return $join; } add_filter( 'posts_where', 'adel_search_where_sku', 10, 2 ); function adel_search_where_sku( $where, $query ) { global $wpdb; // توی ادمین معمولی کاری نکن، ولی برای AJAX اجازه بده if ( is_admin() && ( ! function_exists('wp_doing_ajax') || ! wp_doing_ajax() ) ) { return $where; } $post_types = (array) $query->get( 'post_type' ); if ( ! in_array( 'product', $post_types ) && ! empty( $post_types ) ) { return $where; } $search_term = $query->get( 's' ); if ( empty( $search_term ) ) { return $where; } // سرچ جزئی روی SKU $like = '%' . $wpdb->esc_like( $search_term ) . '%'; $where .= $wpdb->prepare( " OR (sku_pm.meta_value LIKE %s)", $like ); return $where; } // جلوگیری از نتایج تکراری وقتی روی محصول و سرچ هستیم add_filter( 'posts_distinct', 'adel_search_distinct_sku', 10, 2 ); function adel_search_distinct_sku( $distinct, $query ) { if ( is_admin() && ( ! function_exists('wp_doing_ajax') || ! wp_doing_ajax() ) ) { return $distinct; } $post_types = (array) $query->get( 'post_type' ); $search_term = $query->get( 's' ); if ( in_array( 'product', $post_types ) && ! empty( $search_term ) ) { return 'DISTINCT'; } return $distinct; } if (!defined('ABSPATH')) exit; /** * FD OFF - Full (Flatsome friendly) * - نمایش محصولات OFF داخل لیست محصولات همان دسته‌بندی سایت ۱ (بدون اینکه جدا مشخص شود) * - منوی تنظیمات: نمایش اول/آخر + متن و رنگ برچسب + انیمیشن دور کادر (رنگ + شدت) * - زیر هر کارت OFF فقط 3 خط: * 1) قیمت بازار (خط خورده) * 2) سود شما از خرید * 3) قیمت نهایی پرداخت شما * - هیچ تغییری روی سبد خرید/پرداخت سایت ۱ ندارد (فقط لینک به OFF) * * نکته: اگر CK/CS نگذاری هم ممکنه کار کند چون از Store API هم می‌تواند بخواند، * ولی برای رتبه‌بندی/فیلدهای اضافی، CK/CS بهتر است. */ /** ====== تنظیمات اتصال (اختیاری اما پیشنهاد می‌شود) ====== */ define('FD_OFF_BASE', 'https://faryazandecor.com/OFF'); define('FD_OFF_CK', 'ck_...'); // کلید Read define('FD_OFF_CS', 'cs_...'); // سکرت Read /** ========================================================= */ define('FD_OFF_CAT_CACHE_SEC', 3600); define('FD_OFF_PROD_CACHE_SEC', 600); define('FD_OFF_PER_PAGE_FALLBACK', 12); /** option keys */ define('FD_OFF_OPT_GROUP', 'fd_off_opts'); define('FD_OFF_OPT_SHOW_FIRST', 'fd_off_show_first'); // 1/0 define('FD_OFF_OPT_LABEL_ENABLE', 'fd_off_label_enable'); // 1/0 define('FD_OFF_OPT_LABEL_TEXT', 'fd_off_label_text'); // string define('FD_OFF_OPT_LABEL_COLOR', 'fd_off_label_color'); // hex define('FD_OFF_OPT_ANIM_ENABLE', 'fd_off_anim_enable'); // 1/0 define('FD_OFF_OPT_ANIM_COLOR', 'fd_off_anim_color'); // hex define('FD_OFF_OPT_ANIM_INTENSITY', 'fd_off_anim_intensity'); // 0..100 /* ========= Settings Page ========= */ add_action('admin_menu', function () { add_menu_page( 'تنظیمات محصولات OFF', 'محصولات OFF', 'manage_woocommerce', 'fd-off-settings', 'fd_off_render_settings_page', 'dashicons-megaphone', 56 ); }); add_action('admin_init', function () { register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_SHOW_FIRST, [ 'type' => 'integer', 'sanitize_callback' => fn($v) => (int)(!!$v), 'default' => 0, ]); register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_LABEL_ENABLE, [ 'type' => 'integer', 'sanitize_callback' => fn($v) => (int)(!!$v), 'default' => 1, ]); register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_LABEL_TEXT, [ 'type' => 'string', 'sanitize_callback' => function ($v) { $v = wp_strip_all_tags((string)$v); return mb_substr($v, 0, 80); }, 'default' => 'قیمت ویژه اعضا', ]); register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_LABEL_COLOR, [ 'type' => 'string', 'sanitize_callback' => function ($v) { $v = trim((string)$v); if (preg_match('/^#[0-9a-fA-F]{6}$/', $v)) return $v; return '#E53935'; }, 'default' => '#E53935', ]); register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_ANIM_ENABLE, [ 'type' => 'integer', 'sanitize_callback' => fn($v) => (int)(!!$v), 'default' => 1, ]); register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_ANIM_COLOR, [ 'type' => 'string', 'sanitize_callback' => function ($v) { $v = trim((string)$v); if (preg_match('/^#[0-9a-fA-F]{6}$/', $v)) return $v; return '#E53935'; }, 'default' => '#E53935', ]); register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_ANIM_INTENSITY, [ 'type' => 'integer', 'sanitize_callback' => function ($v) { $v = (int)$v; if ($v < 0) $v = 0; if ($v > 100) $v = 100; return $v; }, 'default' => 75, ]); }); function fd_off_render_settings_page() { if (!current_user_can('manage_woocommerce')) return; $show_first = (int)get_option(FD_OFF_OPT_SHOW_FIRST, 0); $label_on = (int)get_option(FD_OFF_OPT_LABEL_ENABLE, 1); $label_txt = (string)get_option(FD_OFF_OPT_LABEL_TEXT, 'قیمت ویژه اعضا'); $label_col = (string)get_option(FD_OFF_OPT_LABEL_COLOR, '#E53935'); $anim_on = (int)get_option(FD_OFF_OPT_ANIM_ENABLE, 1); $anim_color = (string)get_option(FD_OFF_OPT_ANIM_COLOR, '#E53935'); $anim_intensity = (int)get_option(FD_OFF_OPT_ANIM_INTENSITY, 75); ?> <div class="wrap"> <h1>تنظیمات محصولات OFF</h1> <form method="post" action="options.php"> <?php settings_fields(FD_OFF_OPT_GROUP); ?> <table class="form-table" role="presentation"> <tr> <th scope="row">نمایش محصولات OFF اول لیست</th> <td> <label> <input type="checkbox" name="<?php echo esc_attr(FD_OFF_OPT_SHOW_FIRST); ?>" value="1" <?php checked(1, $show_first); ?> /> اگر فعال شود، محصولات OFF قبل از محصولات سایت ۱ نمایش داده می‌شوند. </label> </td> </tr> <tr> <th scope="row">برچسب روی محصولات OFF</th> <td> <label> <input type="checkbox" name="<?php echo esc_attr(FD_OFF_OPT_LABEL_ENABLE); ?>" value="1" <?php checked(1, $label_on); ?> /> برچسب فعال باشد </label> <div style="margin-top:10px;"> <label>متن برچسب:</label><br> <input type="text" class="regular-text" name="<?php echo esc_attr(FD_OFF_OPT_LABEL_TEXT); ?>" value="<?php echo esc_attr($label_txt); ?>" /> </div> <div style="margin-top:10px;"> <label>رنگ پس‌زمینه برچسب:</label><br> <input type="color" name="<?php echo esc_attr(FD_OFF_OPT_LABEL_COLOR); ?>" value="<?php echo esc_attr($label_col); ?>" /> </div> </td> </tr> <tr> <th scope="row">جلب توجه (انیمیشن دور کادر)</th> <td> <label> <input type="checkbox" name="<?php echo esc_attr(FD_OFF_OPT_ANIM_ENABLE); ?>" value="1" <?php checked(1, $anim_on); ?> /> فعال باشد </label> <div style="margin-top:10px;"> <label>رنگ انیمیشن دور کادر:</label><br> <input type="color" name="<?php echo esc_attr(FD_OFF_OPT_ANIM_COLOR); ?>" value="<?php echo esc_attr($anim_color); ?>" /> </div> <div style="margin-top:10px; max-width:420px;"> <label>شدت انیمیشن (درصد): <strong><?php echo (int)$anim_intensity; ?>%</strong></label> <input type="range" name="<?php echo esc_attr(FD_OFF_OPT_ANIM_INTENSITY); ?>" min="0" max="100" step="1" value="<?php echo (int)$anim_intensity; ?>" style="width:100%;" /> </div> </td> </tr> </table> <?php submit_button('ذخیره تنظیمات'); ?> </form> </div> <?php } /* ========= Helpers ========= */ function fd_off_is_admin_view(): bool { return is_user_logged_in() && current_user_can('manage_woocommerce'); } function fd_off_remote_get_json($url, $cache_sec = 600) { if (fd_off_is_admin_view()) $cache_sec = 0; $key = 'fd_off_json_' . md5($url); if ($cache_sec > 0) { $cached = get_transient($key); if ($cached !== false) return $cached; } $res = wp_remote_get($url, ['timeout' => 20, 'headers' => ['Accept' => 'application/json']]); if (is_wp_error($res)) return $res; $code = wp_remote_retrieve_response_code($res); $body = wp_remote_retrieve_body($res); if ($code < 200 || $code >= 300) { return new WP_Error('fd_off_http', 'HTTP ' . $code . ' - ' . wp_strip_all_tags($body)); } $json = json_decode($body, true); if (!is_array($json)) return new WP_Error('fd_off_json', 'Invalid JSON'); if ($cache_sec > 0) set_transient($key, $json, $cache_sec); return $json; } function fd_off_slugs_match($a, $b): bool { return strtolower(rawurldecode((string)$a)) === strtolower(rawurldecode((string)$b)); } function fd_off_get_off_cat_id_by_slug($slug): int { $slug = (string)$slug; if ($slug === '') return 0; // فقط اگر CK/CS گذاشته شده باشد v3 را صدا می‌زنیم (وگرنه می‌افتد روی Store API) $has_keys = (strpos(FD_OFF_CK, 'ck_') === 0) && (strpos(FD_OFF_CS, 'cs_') === 0); if ($has_keys) { $url_v3 = rtrim(FD_OFF_BASE,'/') . '/wp-json/wc/v3/products/categories?per_page=100' . '&consumer_key=' . rawurlencode(FD_OFF_CK) . '&consumer_secret=' . rawurlencode(FD_OFF_CS); $cats = fd_off_remote_get_json($url_v3, FD_OFF_CAT_CACHE_SEC); if (!is_wp_error($cats) && is_array($cats)) { foreach ($cats as $c) { if (!empty($c['slug']) && fd_off_slugs_match($c['slug'], $slug)) return (int)($c['id'] ?? 0); } } } // Store API fallback $url_store = rtrim(FD_OFF_BASE,'/') . '/wp-json/wc/store/v1/products/categories?per_page=100'; $cats2 = fd_off_remote_get_json($url_store, FD_OFF_CAT_CACHE_SEC); if (is_wp_error($cats2)) return 0; foreach ($cats2 as $c) { if (!empty($c['slug']) && fd_off_slugs_match($c['slug'], $slug)) return (int)($c['id'] ?? 0); } return 0; } function fd_off_get_products_for_off_cat($off_cat_id, $per_page, $page) { $has_keys = (strpos(FD_OFF_CK, 'ck_') === 0) && (strpos(FD_OFF_CS, 'cs_') === 0); if ($has_keys) { $url_v3 = rtrim(FD_OFF_BASE,'/') . '/wp-json/wc/v3/products?status=publish' . '&per_page=' . (int)$per_page . '&page=' . (int)$page . '&category=' . (int)$off_cat_id . '&consumer_key=' . rawurlencode(FD_OFF_CK) . '&consumer_secret=' . rawurlencode(FD_OFF_CS); $prods = fd_off_remote_get_json($url_v3, FD_OFF_PROD_CACHE_SEC); if (!is_wp_error($prods) && is_array($prods)) { // اگر rank_90d وجود داشت، بر اساسش مرتب می‌کنه usort($prods, fn($a,$b) => (int)($b['rank_90d'] ?? 0) <=> (int)($a['rank_90d'] ?? 0)); return $prods; } } // Store API fallback $url_store = rtrim(FD_OFF_BASE,'/') . '/wp-json/wc/store/v1/products?per_page='.(int)$per_page . '&page='.(int)$page . '&category='.(int)$off_cat_id; return fd_off_remote_get_json($url_store, FD_OFF_PROD_CACHE_SEC); } function fd_off_best_image($p): string { if (!empty($p['images'][0]['src'])) return (string)$p['images'][0]['src']; // v3 if (!empty($p['images'][0]['thumbnail'])) return (string)$p['images'][0]['thumbnail']; // store if (!empty($p['images'][0]['src'])) return (string)$p['images'][0]['src']; return ''; } function fd_off_product_link($p): string { return !empty($p['permalink']) ? (string)$p['permalink'] : (!empty($p['url']) ? (string)$p['url'] : ''); } function fd_off_num_from_any($v): float { $v = (string)$v; $v = preg_replace('/[^\d\.]/', '', $v); return $v === '' ? 0 : (float)$v; } function fd_off_format_toman($amount): string { $amount = (int) round($amount); if ($amount <= 0) return ''; return number_format_i18n($amount) . ' تومان'; } /** * فقط 3 خط زیر محصول OFF: * - قیمت بازار (فقط عدد خط خورده، متن واضح بماند) * - سود شما از خرید * - قیمت نهایی پرداخت شما */ function fd_off_price_html($p): string { $regular = 0; $sale = 0; $current = 0; // v3 if (isset($p['regular_price']) || isset($p['sale_price']) || isset($p['price'])) { $regular = fd_off_num_from_any($p['regular_price'] ?? ''); $sale = fd_off_num_from_any($p['sale_price'] ?? ''); $current = fd_off_num_from_any($p['price'] ?? ''); } // store fallback if (($regular <= 0 && $sale <= 0 && $current <= 0) && !empty($p['prices'])) { $regular = fd_off_num_from_any($p['prices']['regular_price'] ?? ''); $sale = fd_off_num_from_any($p['prices']['sale_price'] ?? ''); $current = fd_off_num_from_any($p['prices']['price'] ?? ''); } // قیمت نهایی پرداخت شما $final = ($sale > 0) ? $sale : $current; // fallback price_html (اگر رنج قیمت بود یا نشد حساب کرد) $fallback = ''; if (!empty($p['price_html'])) $fallback = (string)$p['price_html']; if (!empty($p['prices']['price_html'])) $fallback = (string)$p['prices']['price_html']; if ($regular <= 0 || $final <= 0 || $regular <= $final) { // اگر نتونستیم سود رو درست حساب کنیم، فقط قیمت نهایی رو نشون بده if ($final > 0) { return '<div class="fd-off-pricebox"><div class="fd-off-lines">' . '<div class="fd-off-line">قیمت نهایی پرداخت شما: <b class="fd-off-final">' . esc_html(fd_off_format_toman($final)) . '</b></div>' . '</div></div>'; } if ($fallback !== '') { return '<div class="fd-off-pricebox"><div class="fd-off-lines">' . wp_kses_post($fallback) . '</div></div>'; } return ''; } $profit = $regular - $final; $out = '<div class="fd-off-pricebox">'; $out .= '<div class="fd-off-lines">'; $out .= '<div class="fd-off-line fd-off-market"><span class="fd-off-k">قیمت بازار:</span> <del class="fd-off-mkt">' . esc_html(fd_off_format_toman($regular)) . '</del></div>'; $out .= '<div class="fd-off-line fd-off-profit"><span class="fd-off-k">سود شما از خرید:</span> <b>' . esc_html(fd_off_format_toman($profit)) . '</b></div>'; $out .= '<div class="fd-off-line fd-off-pay"><span class="fd-off-k">قیمت نهایی پرداخت شما:</span> <b class="fd-off-final">' . esc_html(fd_off_format_toman($final)) . '</b></div>'; $out .= '</div></div>'; return $out; } /* ========= Render items ========= */ function fd_off_render_items($products): string { if (!is_array($products) || empty($products)) return ''; $label_on = (int)get_option(FD_OFF_OPT_LABEL_ENABLE, 1) === 1; $label_txt = (string)get_option(FD_OFF_OPT_LABEL_TEXT, 'قیمت ویژه اعضا'); $label_col = (string)get_option(FD_OFF_OPT_LABEL_COLOR, '#E53935'); $anim_on = (int)get_option(FD_OFF_OPT_ANIM_ENABLE, 1) === 1; ob_start(); foreach ($products as $p) { $name = $p['name'] ?? ''; $link = fd_off_product_link($p); $img = fd_off_best_image($p); $price = fd_off_price_html($p); $anim_class = $anim_on ? ' fd-off-anim' : ''; echo '<div class="product-small col has-hover fd-off-product type-product product-type-external' . esc_attr($anim_class) . '">'; echo '<div class="col-inner">'; if ($label_on && $label_txt !== '') { // برچسب بالاتر از عکس (روی خود عکس نیوفته) echo '<span class="fd-off-badge" style="background:' . esc_attr($label_col) . ';">' . esc_html($label_txt) . '</span>'; } echo '<div class="product-small box">'; echo '<div class="box-image"><div class="image-fade_in_back">'; echo '<a href="' . esc_url($link) . '" target="_blank" rel="nofollow sponsored noopener">'; if ($img) { echo '<img class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail wp-post-image" src="' . esc_url($img) . '" alt="' . esc_attr($name) . '" loading="lazy" />'; } echo '</a>'; echo '</div></div>'; echo '<div class="box-text box-text-products text-center grid-style-2">'; echo '<p class="name product-title"><a href="' . esc_url($link) . '" target="_blank" rel="nofollow sponsored noopener">' . esc_html($name) . '</a></p>'; echo $price; echo '<a class="button" href="' . esc_url($link) . '" target="_blank" rel="nofollow sponsored noopener">مشاهده</a>'; echo '</div>'; echo '</div>'; echo '</div>'; echo '</div>'; } return ob_get_clean(); } /* ========= Build for current category ========= */ function fd_off_build_html_for_current_cat() { if (!class_exists('WooCommerce')) return ''; if (!is_product_category()) return ''; $term = get_queried_object(); if (!$term || empty($term->slug)) return ''; $off_cat_id = fd_off_get_off_cat_id_by_slug($term->slug); if (!$off_cat_id) return ''; $per_page = function_exists('wc_get_loop_prop') ? (int) wc_get_loop_prop('per_page') : 0; if ($per_page <= 0) $per_page = FD_OFF_PER_PAGE_FALLBACK; $paged = max(1, (int)get_query_var('paged')); $products = fd_off_get_products_for_off_cat($off_cat_id, $per_page, $paged); if (is_wp_error($products)) { if (fd_off_is_admin_view()) { return '<div class="woocommerce-error" style="margin:10px 0;">OFF API Error: ' . esc_html($products->get_error_message()) . '</div>'; } return ''; } return fd_off_render_items($products); } /* ========= Inject: FIRST or LAST ========= */ add_filter('woocommerce_product_loop_start', function ($start) { if ((int)get_option(FD_OFF_OPT_SHOW_FIRST, 0) !== 1) return $start; $off_html = fd_off_build_html_for_current_cat(); if (!$off_html) return $start; return $start . $off_html; }, 20); add_filter('woocommerce_product_loop_end', function ($end) { if ((int)get_option(FD_OFF_OPT_SHOW_FIRST, 0) === 1) return $end; $off_html = fd_off_build_html_for_current_cat(); if (!$off_html) return $end; return $off_html . $end; }, 20); /* ========= CSS ========= */ add_action('wp_head', function () { $hex = (string)get_option(FD_OFF_OPT_ANIM_COLOR, '#E53935'); $intensity = (int)get_option(FD_OFF_OPT_ANIM_INTENSITY, 75); if ($intensity < 0) $intensity = 0; if ($intensity > 100) $intensity = 100; if (!preg_match('/^#[0-9a-fA-F]{6}$/', $hex)) $hex = '#E53935'; $r = hexdec(substr($hex, 1, 2)); $g = hexdec(substr($hex, 3, 2)); $b = hexdec(substr($hex, 5, 2)); $alphaBorder = 0.10 + (0.75 * ($intensity / 100)); // 0.10..0.85 $alphaShadow = 0.06 + (0.45 * ($intensity / 100)); // 0.06..0.51 $spreadPx = 6 + (16 * ($intensity / 100)); // 6..22 ?> <style> .fd-off-product .col-inner{ position:relative !important; border-radius:12px; } /* تصویر */ .fd-off-product .box-image{ height:280px !important; display:flex !important; align-items:center !important; justify-content:center !important; overflow:hidden !important; position:relative !important; border-radius:12px; } .fd-off-product .box-image img{ width:100% !important; height:100% !important; object-fit:contain !important; display:block !important; } /* برچسب */ .fd-off-badge{ position:absolute !important; top:-14px !important; right:12px !important; z-index:9999 !important; padding:9px 14px !important; border-radius:999px !important; font-size:13px !important; line-height:1 !important; color:#fff !important; font-weight:900 !important; box-shadow:0 10px 22px rgba(0,0,0,.18) !important; white-space:nowrap !important; max-width:calc(100% - 24px); overflow:hidden; text-overflow:ellipsis; } /* تایپوگرافی نزدیک به Flatsome */ .fd-off-product .box-text{ padding-top:8px !important; } .fd-off-product .product-title a{ font-size:16px !important; font-weight:800 !important; line-height:1.6 !important; } /* باکس قیمت‌ها - خوانا */ .fd-off-pricebox{ margin:10px 0 12px; text-align:right; background:rgba(0,0,0,.03); border:1px solid rgba(0,0,0,.06); border-radius:10px; padding:10px 10px 8px; } .fd-off-lines{ font-size:13px; line-height:2.05; color:#111; } .fd-off-k{ font-weight:800; color:#222; } .fd-off-mkt{ color:#444; font-weight:800; text-decoration-thickness:0.5px; } .fd-off-profit{ color:#b30000; font-weight:900; } .fd-off-final{ font-weight:900; } /* انیمیشن دور کادر */ .fd-off-anim .col-inner:before{ content:""; position:absolute; inset:-2px; border-radius:14px; pointer-events:none; border:1px solid rgba(<?php echo (int)$r;?>,<?php echo (int)$g;?>,<?php echo (int)$b;?>,<?php echo (float)$alphaBorder;?>); animation:fdOffPulse 1.25s ease-in-out infinite; } @keyframes fdOffPulse{ 0% { transform:scale(1); opacity:.35; box-shadow:0 0 0 0 rgba(<?php echo (int)$r;?>,<?php echo (int)$g;?>,<?php echo (int)$b;?>,0); } 50% { transform:scale(1.012);opacity:1; box-shadow:0 0 0 <?php echo (int)$spreadPx; ?>px rgba(<?php echo (int)$r;?>,<?php echo (int)$g;?>,<?php echo (int)$b;?>,<?php echo (float)$alphaShadow;?>); } 100% { transform:scale(1); opacity:.35; box-shadow:0 0 0 0 rgba(<?php echo (int)$r;?>,<?php echo (int)$g;?>,<?php echo (int)$b;?>,0); } } @media (max-width: 900px){ .fd-off-product .box-image{ height:240px !important; } .fd-off-badge{ top:-12px !important; right:10px !important; font-size:12px !important; } .fd-off-product .product-title a{ font-size:15px !important; } } @media (max-width: 520px){ .fd-off-product .box-image{ height:210px !important; } .fd-off-badge{ top:-10px !important; right:8px !important; font-size:12px !important; } .fd-off-lines{ font-size:13px; line-height:2.05; } } </style> <?php }); /* === Front Dot Indicator on Archives (Shop/Category) === */ if (!defined('ABSPATH')) exit; function pcatc_dot_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01'; $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_dot_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_dot_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_dot_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_dot_is_stale($id) { $ts = pcatc_dot_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_dot_cutoff_ts(); } function pcatc_dot_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_dot_is_stale((int)$vid)) return true; } } return false; } // Simple / others: خود محصول return !pcatc_dot_is_stale((int)$product->get_id()); } /** * Add dot next to price on archives (shop/category/tag) */ function pcatc_dot_price_html($price_html, $product) { if (is_admin()) return $price_html; // فقط صفحات لیست محصولات در سایت if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return $price_html; } // اگر قیمت خالیه، چیزی نزن if (trim(wp_strip_all_tags($price_html)) === '') return $price_html; $is_fresh = pcatc_dot_product_is_fresh($product); $dot = $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>'; // نقطه + فاصله + قیمت return $dot . ' ' . $price_html; } add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2); /** CSS for dots (front) */ function pcatc_dot_css() { if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return; echo '<style> .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } add_action('wp_head', 'pcatc_dot_css', 50); /* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */ if (!defined('ABSPATH')) exit; class PCATC_Settings_Snippet { // Options const OPT_CUTOFF = 'pcatc_cutoff_date'; const OPT_MSG = 'pcatc_message'; const OPT_FALLBACK = 'pcatc_use_modified_fallback'; const OPT_SHOW_FRONT = 'pcatc_show_front_status'; const OPT_SHOW_ADMIN = 'pcatc_show_admin_status'; const OPT_TEXT_FRESH = 'pcatc_text_fresh'; const OPT_TEXT_STALE = 'pcatc_text_stale'; // Meta const META = '_pcatc_price_last_updated'; public function __construct() { // Admin settings UI add_action('admin_menu', [$this, 'add_settings_page']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100); // Stamp when price changes add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); add_action('added_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); // Block add to cart + notices add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5); add_action('woocommerce_before_cart', [$this,'cart_checkout_notice']); add_action('woocommerce_before_checkout_form', [$this,'cart_checkout_notice']); // Front indicators add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11); add_filter('woocommerce_available_variation', [$this,'add_variation_status_data'], 10, 3); add_action('wp_enqueue_scripts', [$this,'enqueue_front_js']); // Admin list indicator add_filter('manage_edit-product_columns', [$this,'add_admin_column'], 30); add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2); add_action('admin_head', [$this,'admin_column_css']); } /* ---------- Defaults ---------- */ private function default_cutoff(): string { return '2026-01-01'; } private function default_msg(): string { return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.'; } private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; } private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; } /* ---------- Options getters ---------- */ private function get_cutoff_date(): string { $val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff()); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff(); return $val; } private function get_msg(): string { $val = (string) get_option(self::OPT_MSG, $this->default_msg()); return $val !== '' ? $val : $this->default_msg(); } private function use_fallback(): bool { return get_option(self::OPT_FALLBACK, 'yes') === 'yes'; } private function show_front(): bool { return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes'; } private function show_admin(): bool { return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes'; } private function text_fresh(): string { $val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh()); return $val !== '' ? $val : $this->default_text_fresh(); } private function text_stale(): string { $val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale()); return $val !== '' ? $val : $this->default_text_stale(); } /* ---------- Cutoff timestamp ---------- */ private function cutoff_ts(): int { $dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } /* ---------- Price update stamp ---------- */ public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void { $type = get_post_type($post_id); if (!in_array($type, ['product','product_variation'], true)) return; if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return; update_post_meta($post_id, self::META, time()); } private function last_update_ts($id): int { $ts = (int) get_post_meta($id, self::META, true); if ($ts > 0) return $ts; if ($this->use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } private function is_stale($id): bool { $ts = $this->last_update_ts($id); if ($ts <= 0) return true; return $ts < $this->cutoff_ts(); } private function status_payload_for($id): array { $stale = $this->is_stale($id); return [ 'is_stale' => $stale ? 1 : 0, 'text' => $stale ? $this->text_stale() : $this->text_fresh(), 'class' => $stale ? 'pcatc-stale' : 'pcatc-fresh', ]; } /* ---------- WooCommerce blocking ---------- */ public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) { $target_id = $variation_id ? (int)$variation_id : (int)$product_id; if ($this->is_stale($target_id)) { wc_add_notice($this->get_msg(), 'error'); return false; } return $passed; } public function cart_checkout_notice(): void { if (!function_exists('WC') || !WC()->cart) return; foreach (WC()->cart->get_cart() as $item) { $target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id']; if ($this->is_stale($target_id)) { wc_print_notice($this->get_msg(), 'error'); break; } } } /* ---------- Front status (simple + variable dynamic) ---------- */ public function render_front_status_block(): void { if (!$this->show_front() || !is_product()) return; global $product; if (!$product instanceof WC_Product) return; // For simple products, render fixed status. // For variable products, we render a container that JS will update on variation selection. $is_variable = $product->is_type('variable'); $payload = $this->status_payload_for($product->get_id()); $text = esc_html($payload['text']); $cls = esc_attr($payload['class']); echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">'; echo $is_variable ? '' : $text; echo '</div>'; } public function add_variation_status_data($variation_data, $product, $variation) { if (!$this->show_front()) return $variation_data; $vid = $variation->get_id(); $p = $this->status_payload_for($vid); $variation_data['pcatc_is_stale'] = $p['is_stale']; $variation_data['pcatc_text'] = $p['text']; $variation_data['pcatc_class'] = $p['class']; return $variation_data; } public function enqueue_front_js(): void { if (!$this->show_front() || !is_product()) return; wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true); wp_enqueue_script('pcatc-front'); // Inline CSS (front) $css = " #pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;} #pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;} "; wp_add_inline_style('woocommerce-inline', $css); // JS: update status when variation changes $js = <<<JS jQuery(function($){ var box = $('#pcatc-price-status'); if(!box.length) return; var form = $('form.variations_form'); if(!form.length) return; // simple product -> no need function setStatus(v){ if(!v || typeof v.pcatc_is_stale === 'undefined'){ // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده) box.text(''); box.removeClass('pcatc-fresh pcatc-stale'); return; } box.text(v.pcatc_text || ''); box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || ''); } form.on('found_variation', function(e, variation){ setStatus(variation); }); form.on('reset_data', function(){ setStatus(null); }); }); JS; wp_add_inline_script('pcatc-front', $js); } /* ---------- Admin list column (green/red dot) ---------- */ public function add_admin_column($columns) { if (!$this->show_admin()) return $columns; // Insert near price column if possible $new = []; foreach ($columns as $key => $label) { $new[$key] = $label; if ($key === 'price') { $new['pcatc_status'] = 'وضعیت قیمت'; } } if (!isset($new['pcatc_status'])) { $new['pcatc_status'] = 'وضعیت قیمت'; } return $new; } public function render_admin_column($column, $post_id) { if (!$this->show_admin()) return; if ($column !== 'pcatc_status') return; // For variable product: if ANY variation is fresh => green else red $product = wc_get_product($post_id); if (!$product) return; $is_fresh = false; if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; } } } } else { $is_fresh = !$this->is_stale($post_id); } echo $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>'; } public function admin_column_css() { if (!$this->show_admin()) return; echo '<style> .column-pcatc_status{width:80px;text-align:center;} .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } /* ---------- Admin settings page ---------- */ public function add_settings_page(): void { add_options_page( 'تنظیمات قفل خرید بر اساس تاریخ', 'قفل خرید (تاریخ قیمت)', 'manage_options', 'pcatc-settings', [$this, 'render_settings_page'] ); } public function register_settings(): void { register_setting('pcatc_settings_group', self::OPT_CUTOFF, [ 'type' => 'string', 'sanitize_callback' => function($v){ $v = trim((string)$v); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff(); } ]); register_setting('pcatc_settings_group', self::OPT_MSG, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_textarea_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_FALLBACK, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); } public function render_settings_page(): void { if (!current_user_can('manage_options')) return; $cutoff = esc_attr($this->get_cutoff_date()); $msg = esc_textarea($this->get_msg()); $fb = $this->use_fallback() ? 'yes' : 'no'; $sf = $this->show_front() ? 'yes' : 'no'; $sa = $this->show_admin() ? 'yes' : 'no'; $tf = esc_attr($this->text_fresh()); $ts = esc_attr($this->text_stale()); ?> <div class="wrap"> <h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1> <form method="post" action="options.php"> <?php settings_fields('pcatc_settings_group'); ?> <table class="form-table" role="presentation"> <tr> <th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th> <td> <input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>"> <p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th> <td> <textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea> <p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p> </td> </tr> <tr> <th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th> <td> <label> <input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>> اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن. </label> </td> </tr> <tr> <th scope="row">نمایش وضعیت قیمت</th> <td> <label style="display:block;margin-bottom:6px;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>> نمایش نقطه سبز/قرمز در لیست محصولات (پنل) </label> <label style="display:block;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>> نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای) </label> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;"> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;"> </td> </tr> </table> <?php submit_button('ذخیره تنظیمات'); ?> </form> </div> <?php } /* ---------- Admin bar shortcut ---------- */ public function admin_bar_link($admin_bar): void { if (!is_admin_bar_showing() || !current_user_can('manage_options')) return; $admin_bar->add_node([ 'id' => 'pcatc_settings_link', 'title' => 'تنظیمات قفل خرید', 'href' => admin_url('options-general.php?page=pcatc-settings'), ]); } } new PCATC_Settings_Snippet(); if (!defined('ABSPATH')) exit; if (!class_exists('KCN_Keep_Like_Code_App_V2')) { class KCN_Keep_Like_Code_App_V2 { private $option_key = 'kcn_stable_notes_data_v2'; public function __construct() { add_shortcode('kcn_code_app', array($this, 'render_app')); add_action('wp_ajax_kcn_delete_note', array($this, 'ajax_delete_note')); add_action('wp_ajax_nopriv_kcn_delete_note', array($this, 'ajax_delete_note')); add_action('wp_ajax_kcn_toggle_fav', array($this, 'ajax_toggle_fav')); add_action('wp_ajax_nopriv_kcn_toggle_fav', array($this, 'ajax_toggle_fav')); add_action('wp_ajax_kcn_update_title', array($this, 'ajax_update_title')); add_action('wp_ajax_nopriv_kcn_update_title', array($this, 'ajax_update_title')); add_action('wp_ajax_kcn_bulk_delete', array($this, 'ajax_bulk_delete')); add_action('wp_ajax_nopriv_kcn_bulk_delete', array($this, 'ajax_bulk_delete')); } private function get_notes() { $notes = get_option($this->option_key, array()); return is_array($notes) ? $notes : array(); } private function save_notes($notes) { update_option($this->option_key, array_values($notes), false); } private function sanitize_code($code) { return wp_unslash($code); } private function handle_submit() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { return; } if (!isset($_POST['kcn_action']) || $_POST['kcn_action'] !== 'save_note') { return; } if (!isset($_POST['kcn_nonce']) || !wp_verify_nonce($_POST['kcn_nonce'], 'kcn_save_note')) { return; } $title = isset($_POST['kcn_title']) ? sanitize_text_field(wp_unslash($_POST['kcn_title'])) : ''; $language = isset($_POST['kcn_language']) ? sanitize_text_field(wp_unslash($_POST['kcn_language'])) : 'text'; $code = isset($_POST['kcn_code']) ? $this->sanitize_code($_POST['kcn_code']) : ''; if ($title === '' || trim($code) === '') { return; } $notes = $this->get_notes(); $notes[] = array( 'id' => uniqid('kcn_', true), 'title' => $title, 'language' => $language, 'code' => $code, 'created' => current_time('mysql'), 'fav' => 0, ); $this->save_notes($notes); } private function find_note_index($note_id, $notes) { foreach ($notes as $i => $note) { if (isset($note['id']) && $note['id'] === $note_id) { return $i; } } return -1; } public function ajax_delete_note() { check_ajax_referer('kcn_ajax_nonce', 'nonce'); $note_id = isset($_POST['note_id']) ? sanitize_text_field(wp_unslash($_POST['note_id'])) : ''; if (!$note_id) { wp_send_json_error(array('message' => 'شناسه نامعتبر است')); } $notes = $this->get_notes(); $new_notes = array(); foreach ($notes as $note) { if (!isset($note['id']) || $note['id'] !== $note_id) { $new_notes[] = $note; } } $this->save_notes($new_notes); wp_send_json_success(array('note_id' => $note_id)); } public function ajax_toggle_fav() { check_ajax_referer('kcn_ajax_nonce', 'nonce'); $note_id = isset($_POST['note_id']) ? sanitize_text_field(wp_unslash($_POST['note_id'])) : ''; if (!$note_id) { wp_send_json_error(array('message' => 'شناسه نامعتبر است')); } $notes = $this->get_notes(); $index = $this->find_note_index($note_id, $notes); if ($index < 0) { wp_send_json_error(array('message' => 'یادداشت پیدا نشد')); } $notes[$index]['fav'] = empty($notes[$index]['fav']) ? 1 : 0; $fav = $notes[$index]['fav']; $this->save_notes($notes); wp_send_json_success(array('fav' => $fav)); } public function ajax_update_title() { check_ajax_referer('kcn_ajax_nonce', 'nonce'); $note_id = isset($_POST['note_id']) ? sanitize_text_field(wp_unslash($_POST['note_id'])) : ''; $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : ''; if (!$note_id || $title === '') { wp_send_json_error(array('message' => 'اطلاعات ناقص است')); } $notes = $this->get_notes(); $index = $this->find_note_index($note_id, $notes); if ($index < 0) { wp_send_json_error(array('message' => 'یادداشت پیدا نشد')); } $notes[$index]['title'] = $title; $this->save_notes($notes); wp_send_json_success(array('title' => $title)); } public function ajax_bulk_delete() { check_ajax_referer('kcn_ajax_nonce', 'nonce'); $ids = isset($_POST['ids']) ? (array) $_POST['ids'] : array(); $ids = array_map('sanitize_text_field', $ids); $ids = array_filter($ids); if (empty($ids)) { wp_send_json_error(array('message' => 'موردی انتخاب نشده')); } $notes = $this->get_notes(); $new_notes = array(); foreach ($notes as $note) { if (!isset($note['id']) || !in_array($note['id'], $ids, true)) { $new_notes[] = $note; } } $this->save_notes($new_notes); wp_send_json_success(array('deleted_ids' => $ids)); } private function render_styles() { ?> <style> .kcn-wrap{ max-width:1000px; margin:20px auto; padding:14px; font-family:Tahoma, Arial, sans-serif; direction:rtl; } .kcn-card{ background:#fff; border:1px solid #e5e7eb; border-radius:16px; padding:14px; margin-bottom:16px; box-shadow:0 4px 14px rgba(0,0,0,.05); } .kcn-title{ margin:0 0 14px; font-size:19px; font-weight:700; color:#111827; } .kcn-field{margin-bottom:14px;} .kcn-label{ display:block; margin-bottom:8px; font-weight:600; color:#111827; font-size:14px; } .kcn-input,.kcn-select,.kcn-textarea{ width:100%; box-sizing:border-box; border:1px solid #d1d5db; border-radius:12px; background:#fff; color:#111827; padding:12px 14px; font-size:14px; outline:none; } .kcn-textarea{ min-height:220px; resize:vertical; font-family:Consolas, Monaco, monospace; line-height:1.7; direction:ltr; text-align:left; white-space:pre; unicode-bidi:plaintext; } .kcn-btn{ display:inline-block; border:none; border-radius:12px; padding:9px 14px; cursor:pointer; font-size:13px; font-weight:700; text-decoration:none; } .kcn-btn-primary{background:#2563eb;color:#fff;} .kcn-btn-light{background:#f3f4f6;color:#111827;} .kcn-btn-danger{background:#dc2626;color:#fff;} .kcn-btn-star{ background:#fff7ed; color:#9a3412; border:1px solid #fdba74; } .kcn-toolbar{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; } .kcn-list{ display:grid; gap:12px; } .kcn-note{ background:#fff; border:1px solid #e5e7eb; border-radius:16px; overflow:hidden; position:relative; } .kcn-note.fav{ border-color:#f59e0b; box-shadow:0 0 0 2px rgba(245,158,11,.12); } .kcn-note-head{ padding:14px; } .kcn-row-top{ display:flex; align-items:flex-start; gap:10px; } .kcn-check{ margin-top:3px; flex:0 0 auto; } .kcn-main{ flex:1 1 auto; min-width:0; } .kcn-note-name{ font-weight:700; color:#111827; margin-bottom:6px; font-size:15px; word-break:break-word; } .kcn-note-name-input{ width:100%; border:1px solid #d1d5db; border-radius:10px; padding:8px 10px; font-size:14px; color:#111827; background:#fff; box-sizing:border-box; } .kcn-note-sub{ font-size:12px; color:#6b7280; margin-bottom:10px; } .kcn-preview{ font-family:Consolas, Monaco, monospace; font-size:12px; line-height:1.7; color:#374151; background:#f9fafb; border:1px solid #eef2f7; border-radius:12px; padding:12px; direction:ltr; text-align:left; white-space:pre-wrap; word-break:break-word; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; } .kcn-full{ display:none; margin-top:10px; } .kcn-full.open{ display:block; } .kcn-code{ margin:0; padding:14px; background:#ffffff; border:1px solid #e5e7eb; border-radius:12px; color:#111827; font-size:12px; line-height:1.75; direction:ltr; text-align:left; white-space:pre-wrap; word-break:break-word; overflow:auto; font-family:Consolas, Monaco, monospace; max-height:420px; } .kcn-actions{ display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; } .kcn-empty{ color:#6b7280; font-size:14px; } .kcn-save-title-wrap{ display:none; gap:8px; margin-bottom:10px; } .kcn-save-title-wrap.open{ display:flex; } .kcn-status{ font-size:12px; color:#16a34a; margin-top:6px; display:none; } .kcn-status.show{ display:block; } @media (max-width:768px){ .kcn-wrap{padding:10px;} .kcn-card{padding:12px;} .kcn-btn{flex:1 1 100%;text-align:center;} .kcn-code{max-height:300px;} .kcn-row-top{align-items:flex-start;} } </style> <?php } private function render_script() { $ajax_url = admin_url('admin-ajax.php'); $nonce = wp_create_nonce('kcn_ajax_nonce'); ?> <script> document.addEventListener('DOMContentLoaded', function(){ var wrap = document.querySelector('.kcn-wrap'); if (!wrap) return; function postAjax(action, data) { var fd = new FormData(); fd.append('action', action); fd.append('nonce', '<?php echo esc_js($nonce); ?>'); Object.keys(data).forEach(function(key){ if (Array.isArray(data[key])) { data[key].forEach(function(v){ fd.append(key + '[]', v); }); } else { fd.append(key, data[key]); } }); return fetch('<?php echo esc_url($ajax_url); ?>', { method: 'POST', body: fd, credentials: 'same-origin' }).then(function(r){ return r.json(); }); } document.addEventListener('click', function(e){ var toggleBtn = e.target.closest('.kcn-toggle-btn'); if (toggleBtn) { e.preventDefault(); var targetId = toggleBtn.getAttribute('data-target'); var box = document.getElementById(targetId); if (!box) return; if (box.classList.contains('open')) { box.classList.remove('open'); toggleBtn.textContent = 'نمایش بیشتر'; } else { box.classList.add('open'); toggleBtn.textContent = 'بستن'; } return; } var copyBtn = e.target.closest('.kcn-copy-btn'); if (copyBtn) { e.preventDefault(); var targetId = copyBtn.getAttribute('data-target'); var codeEl = document.getElementById(targetId); if (!codeEl) return; var text = codeEl.innerText || codeEl.textContent || ''; var oldText = copyBtn.textContent; function done() { copyBtn.textContent = 'کپی شد'; setTimeout(function(){ copyBtn.textContent = oldText; }, 1500); } if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(done).catch(function(){ fallbackCopy(text, done); }); } else { fallbackCopy(text, done); } function fallbackCopy(text, callback) { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand('copy'); callback(); } catch(err){} document.body.removeChild(ta); } return; } var delBtn = e.target.closest('.kcn-delete-btn'); if (delBtn) { e.preventDefault(); var noteId = delBtn.getAttribute('data-id'); var card = delBtn.closest('.kcn-note'); if (!noteId || !card) return; delBtn.disabled = true; delBtn.textContent = 'در حال حذف...'; postAjax('kcn_delete_note', {note_id: noteId}).then(function(res){ if (res && res.success) { card.remove(); } else { delBtn.disabled = false; delBtn.textContent = 'حذف'; } }).catch(function(){ delBtn.disabled = false; delBtn.textContent = 'حذف'; }); return; } var favBtn = e.target.closest('.kcn-fav-btn'); if (favBtn) { e.preventDefault(); var noteId = favBtn.getAttribute('data-id'); var card = favBtn.closest('.kcn-note'); if (!noteId || !card) return; postAjax('kcn_toggle_fav', {note_id: noteId}).then(function(res){ if (res && res.success) { if (parseInt(res.data.fav, 10) === 1) { card.classList.add('fav'); favBtn.textContent = '★ اوکیه'; } else { card.classList.remove('fav'); favBtn.textContent = '☆ علامت بزن'; } } }); return; } var editBtn = e.target.closest('.kcn-edit-title-btn'); if (editBtn) { e.preventDefault(); var note = editBtn.closest('.kcn-note'); if (!note) return; var editBox = note.querySelector('.kcn-save-title-wrap'); if (editBox) editBox.classList.toggle('open'); return; } var saveTitleBtn = e.target.closest('.kcn-save-title-btn'); if (saveTitleBtn) { e.preventDefault(); var note = saveTitleBtn.closest('.kcn-note'); if (!note) return; var noteId = saveTitleBtn.getAttribute('data-id'); var input = note.querySelector('.kcn-note-name-input'); var titleEl = note.querySelector('.kcn-note-name'); var statusEl = note.querySelector('.kcn-status'); var editBox = note.querySelector('.kcn-save-title-wrap'); if (!noteId || !input || !titleEl) return; var newTitle = (input.value || '').trim(); if (!newTitle) return; saveTitleBtn.disabled = true; saveTitleBtn.textContent = 'در حال ذخیره...'; postAjax('kcn_update_title', { note_id: noteId, title: newTitle }).then(function(res){ saveTitleBtn.disabled = false; saveTitleBtn.textContent = 'ذخیره عنوان'; if (res && res.success) { titleEl.textContent = res.data.title; if (statusEl) { statusEl.textContent = 'عنوان ذخیره شد'; statusEl.classList.add('show'); setTimeout(function(){ statusEl.classList.remove('show'); }, 1500); } if (editBox) editBox.classList.remove('open'); } }).catch(function(){ saveTitleBtn.disabled = false; saveTitleBtn.textContent = 'ذخیره عنوان'; }); return; } var bulkDelBtn = e.target.closest('.kcn-bulk-delete-btn'); if (bulkDelBtn) { e.preventDefault(); var checked = Array.prototype.slice.call(document.querySelectorAll('.kcn-bulk-check:checked')); var ids = checked.map(function(ch){ return ch.value; }); if (!ids.length) return; bulkDelBtn.disabled = true; bulkDelBtn.textContent = 'در حال حذف...'; postAjax('kcn_bulk_delete', {ids: ids}).then(function(res){ bulkDelBtn.disabled = false; bulkDelBtn.textContent = 'حذف انتخاب‌شده‌ها'; if (res && res.success) { ids.forEach(function(id){ var card = document.querySelector('.kcn-note[data-id="' + id + '"]'); if (card) card.remove(); }); } }).catch(function(){ bulkDelBtn.disabled = false; bulkDelBtn.textContent = 'حذف انتخاب‌شده‌ها'; }); return; } }); }); </script> <?php } public function render_app() { $this->handle_submit(); $notes = $this->get_notes(); usort($notes, function($a, $b){ $af = !empty($a['fav']) ? 1 : 0; $bf = !empty($b['fav']) ? 1 : 0; if ($af !== $bf) { return $bf - $af; } return strcmp($b['created'], $a['created']); }); ob_start(); $this->render_styles(); $this->render_script(); ?> <div class="kcn-wrap"> <div class="kcn-card"> <h2 class="kcn-title">ارسال کد جدید</h2> <form method="post"> <div class="kcn-field"> <label class="kcn-label">عنوان</label> <input class="kcn-input" type="text" name="kcn_title" placeholder="مثلاً: کد فرم تماس" required> </div> <div class="kcn-field"> <label class="kcn-label">زبان</label> <select class="kcn-select" name="kcn_language"> <option value="text">Text</option> <option value="php">PHP</option> <option value="js">JavaScript</option> <option value="html">HTML</option> <option value="css">CSS</option> <option value="json">JSON</option> <option value="sql">SQL</option> <option value="python">Python</option> </select> </div> <div class="kcn-field"> <label class="kcn-label">کد</label> <textarea class="kcn-textarea" name="kcn_code" placeholder="کد را اینجا وارد کنید..." required></textarea> </div> <input type="hidden" name="kcn_action" value="save_note"> <?php wp_nonce_field('kcn_save_note', 'kcn_nonce'); ?> <button type="submit" class="kcn-btn kcn-btn-primary">ذخیره کد</button> </form> </div> <div class="kcn-card"> <div class="kcn-toolbar"> <button type="button" class="kcn-btn kcn-btn-danger kcn-bulk-delete-btn">حذف انتخاب‌شده‌ها</button> </div> <h2 class="kcn-title">لیست کدهای ذخیره‌شده</h2> <?php if (empty($notes)) : ?> <div class="kcn-empty">هنوز کدی ذخیره نشده است.</div> <?php else : ?> <div class="kcn-list"> <?php foreach ($notes as $index => $note) : $code_id = 'kcn_code_' . md5($note['id'] . '_' . $index); $full_id = 'kcn_full_' . md5($note['id'] . '_full_' . $index); $is_fav = !empty($note['fav']); ?> <div class="kcn-note <?php echo $is_fav ? 'fav' : ''; ?>" data-id="<?php echo esc_attr($note['id']); ?>"> <div class="kcn-note-head"> <div class="kcn-row-top"> <div class="kcn-check"> <input type="checkbox" class="kcn-bulk-check" value="<?php echo esc_attr($note['id']); ?>"> </div> <div class="kcn-main"> <div class="kcn-note-name"><?php echo esc_html($note['title']); ?></div> <div class="kcn-save-title-wrap"> <input type="text" class="kcn-note-name-input" value="<?php echo esc_attr($note['title']); ?>"> <button type="button" class="kcn-btn kcn-btn-light kcn-save-title-btn" data-id="<?php echo esc_attr($note['id']); ?>">ذخیره عنوان</button> </div> <div class="kcn-status"></div> <div class="kcn-note-sub"> <?php echo esc_html(strtoupper($note['language'])); ?> - <?php echo esc_html($note['created']); ?> </div> <div class="kcn-preview"><?php echo esc_html($note['code']); ?></div> <div id="<?php echo esc_attr($full_id); ?>" class="kcn-full"> <pre id="<?php echo esc_attr($code_id); ?>" class="kcn-code"><?php echo esc_html($note['code']); ?></pre> </div> <div class="kcn-actions"> <button type="button" class="kcn-btn kcn-btn-light kcn-toggle-btn" data-target="<?php echo esc_attr($full_id); ?>">نمایش بیشتر</button> <button type="button" class="kcn-btn kcn-btn-light kcn-copy-btn" data-target="<?php echo esc_attr($code_id); ?>">کپی کل کد</button> <button type="button" class="kcn-btn kcn-btn-light kcn-edit-title-btn">ویرایش عنوان</button> <button type="button" class="kcn-btn kcn-btn-star kcn-fav-btn" data-id="<?php echo esc_attr($note['id']); ?>"> <?php echo $is_fav ? '★ اوکیه' : '☆ علامت بزن'; ?> </button> <button type="button" class="kcn-btn kcn-btn-danger kcn-delete-btn" data-id="<?php echo esc_attr($note['id']); ?>">حذف</button> </div> </div> </div> </div> </div> <?php endforeach; ?> </div> <?php endif; ?> </div> </div> <?php return ob_get_clean(); } } new KCN_Keep_Like_Code_App_V2(); } add_action('wp_footer', function() { echo ' <style> .rubika-wrap { position: fixed; right: 15px; bottom: 15px; width: 100px; z-index: 99999; } .rubika-float { display: block; width: 100%; background: none; border: none; border-radius: 0; box-shadow: none; padding: 0; transition: transform 0.2s ease-in-out; } .rubika-float:hover { transform: scale(1.05); } .rubika-float img { width: 100%; height: auto; display: block; } .rubika-close { position: absolute; top: -20px; left: 0; width: auto !important; height: auto !important; min-width: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; background: transparent !important; border: none !important; box-shadow: none !important; outline: none !important; color: #000 !important; font-size: 26px !important; font-weight: bold; line-height: 1 !important; cursor: pointer; z-index: 100000; appearance: none; -webkit-appearance: none; } .rubika-close:hover, .rubika-close:focus, .rubika-close:active { background: transparent !important; border: none !important; box-shadow: none !important; outline: none !important; color: #000 !important; } </style> <div class="rubika-wrap" id="rubikaWrap"> <button type="button" class="rubika-close" id="rubikaClose" aria-label="بستن">×</button> <a href="https://rubika.ir/faryazan365" class="rubika-float" target="_blank" rel="noopener noreferrer"> <img src="https://faryazandecor.com/wp-content/uploads/2026/03/logo01@3x-2.png" alt="Rubika"> </a> </div> <script> document.addEventListener("DOMContentLoaded", function() { var closeBtn = document.getElementById("rubikaClose"); var rubikaWrap = document.getElementById("rubikaWrap"); if (closeBtn && rubikaWrap) { closeBtn.addEventListener("click", function() { rubikaWrap.style.display = "none"; }); } }); </script> '; }); if (!defined('ABSPATH')) exit; /** * فقط ادمین */ function qv_is_admin_user() { return current_user_can('manage_woocommerce') || current_user_can('administrator'); } /** * لیبل attribute */ function qv_get_attribute_label_safe($name, $product = null) { if (function_exists('wc_attribute_label')) { $label = wc_attribute_label($name, $product); if (!empty($label)) return $label; } if (strpos($name, 'pa_') === 0) { $name = str_replace('pa_', '', $name); } return ucfirst(str_replace(array('-', '_'), ' ', $name)); } /** * متن خوانا برای option */ function qv_get_readable_option_label($attribute_name, $option_value) { if ($option_value === '' || $option_value === null) { return ''; } if (taxonomy_exists($attribute_name)) { $term = get_term_by('slug', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } $term = get_term_by('name', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } } $decoded = rawurldecode($option_value); $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8'); return $decoded; } /** * همه attributeهای قابل انتخاب * - هم attributeهای روی خود محصول * - هم همه attributeهای سراسری ووکامرس */ function qv_get_all_selectable_attributes($product) { $result = array(); $map = array(); /** * 1) اول attributeهای خود محصول */ $product_attributes = $product->get_attributes(); if (!empty($product_attributes)) { foreach ($product_attributes as $attribute_key => $attribute_obj) { if (!is_a($attribute_obj, 'WC_Product_Attribute')) { continue; } $attribute_name = $attribute_obj->get_name(); $label = qv_get_attribute_label_safe($attribute_name, $product); $options = array(); if ($attribute_obj->is_taxonomy()) { $terms = wc_get_product_terms($product->get_id(), $attribute_name, array('fields' => 'all')); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => $term->slug, 'label' => $term->name, ); } } } else { $raw_options = $attribute_obj->get_options(); if (!empty($raw_options)) { foreach ($raw_options as $opt) { if ($opt === '' || $opt === null) continue; $options[] = array( 'value' => $opt, 'label' => qv_get_readable_option_label($attribute_name, $opt), ); } } } if (!isset($map[$attribute_name])) { $map[$attribute_name] = array( 'name' => $attribute_name, 'label' => $label, 'options' => array(), ); } foreach ($options as $opt) { $map[$attribute_name]['options'][(string)$opt['value']] = $opt; } } } /** * 2) همه attributeهای سراسری ووکامرس */ $global_attributes = function_exists('wc_get_attribute_taxonomies') ? wc_get_attribute_taxonomies() : array(); if (!empty($global_attributes)) { foreach ($global_attributes as $ga) { if (empty($ga->attribute_name)) continue; $taxonomy = wc_attribute_taxonomy_name($ga->attribute_name); if (!taxonomy_exists($taxonomy)) continue; $label = !empty($ga->attribute_label) ? $ga->attribute_label : qv_get_attribute_label_safe($taxonomy, $product); if (!isset($map[$taxonomy])) { $map[$taxonomy] = array( 'name' => $taxonomy, 'label' => $label, 'options' => array(), ); } $terms = get_terms(array( 'taxonomy' => $taxonomy, 'hide_empty' => false, )); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $map[$taxonomy]['options'][(string)$term->slug] = array( 'value' => $term->slug, 'label' => $term->name, ); } } } } foreach ($map as $attribute_name => $item) { if (!empty($item['options'])) { $item['options'] = array_values($item['options']); $result[] = $item; } } return $result; } /** * تبدیل محصول به variable */ function qv_ensure_variable_product($product_id) { $product = wc_get_product($product_id); if (!$product) return false; if ($product->is_type('variable')) { return true; } wp_set_object_terms($product_id, 'variable', 'product_type'); clean_post_cache($product_id); $product = wc_get_product($product_id); return ($product && $product->is_type('variable')); } /** * افزودن attribute به محصول اگر نبود */ function qv_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') { $product = wc_get_product($product_id); if (!$product) return false; $attributes = $product->get_attributes(); if (isset($attributes[$attribute_name])) { $attr_obj = $attributes[$attribute_name]; if (is_a($attr_obj, 'WC_Product_Attribute')) { $attr_obj->set_visible(true); $attr_obj->set_variation(true); if (!$attr_obj->is_taxonomy() && $attribute_value !== '') { $options = $attr_obj->get_options(); if (!in_array($attribute_value, $options, true)) { $options[] = $attribute_value; $attr_obj->set_options($options); } } $attributes[$attribute_name] = $attr_obj; $product->set_attributes($attributes); $product->save(); } return true; } $new_attr = new WC_Product_Attribute(); if (taxonomy_exists($attribute_name)) { $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0; $new_attr->set_id($taxonomy_id); $new_attr->set_name($attribute_name); $new_attr->set_options(array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } else { $new_attr->set_id(0); $new_attr->set_name($attribute_name); $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } $attributes[$attribute_name] = $new_attr; $product->set_attributes($attributes); $product->save(); return true; } /** * variation تکراری */ function qv_variation_exists($product_id, $variation_attributes) { $children = get_posts(array( 'post_parent' => $product_id, 'post_type' => 'product_variation', 'post_status' => array('publish', 'private'), 'numberposts' => -1, 'fields' => 'ids', )); if (empty($children)) return false; foreach ($children as $variation_id) { $same = true; foreach ($variation_attributes as $key => $value) { $existing = get_post_meta($variation_id, $key, true); if ((string)$existing !== (string)$value) { $same = false; break; } } if ($same) { return true; } } return false; } /** * فرم */ function qv_render_quick_variation_form() { if (!is_product()) return; if (!qv_is_admin_user()) return; global $product; if (!$product || !is_a($product, 'WC_Product')) return; $attributes = qv_get_all_selectable_attributes($product); if (empty($attributes)) return; ?> <div class="qv-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;"> <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3> <form method="post" class="qv-quick-variation-form" autocomplete="off" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <?php wp_nonce_field('qv_quick_variation_action', 'qv_quick_variation_nonce'); ?> <input type="hidden" name="qv_product_id" value="<?php echo esc_attr($product->get_id()); ?>"> <div> <label style="display:block;margin-bottom:6px;">ویژگی اول</label> <select name="qv_attr1" id="qv_attr1_custom" style="width:100%;padding:8px;"> <option value="">انتخاب ویژگی</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label> <select name="qv_val1" id="qv_val1_custom" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div> <label style="display:block;margin-bottom:6px;">ویژگی دوم</label> <select name="qv_attr2" id="qv_attr2_custom" style="width:100%;padding:8px;"> <option value="">بدون ویژگی دوم</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label> <select name="qv_val2" id="qv_val2_custom" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div style="grid-column:1/-1;"> <label style="display:block;margin-bottom:6px;">قیمت</label> <input type="number" step="0.01" min="0" name="qv_price" required style="width:100%;padding:8px;"> </div> <div style="grid-column:1/-1;"> <button type="submit" name="qv_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;"> افزودن تنوع </button> </div> </form> </div> <script> (function(){ var attributes = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>; var attr1 = document.getElementById('qv_attr1_custom'); var val1 = document.getElementById('qv_val1_custom'); var attr2 = document.getElementById('qv_attr2_custom'); var val2 = document.getElementById('qv_val2_custom'); if (!attr1 || !val1 || !attr2 || !val2) return; function findAttribute(name) { for (var i = 0; i < attributes.length; i++) { if (attributes[i].name === name) return attributes[i]; } return null; } function fillValues(attrSelect, valueSelect) { var attrName = attrSelect.value; var previousValue = valueSelect.value || ''; valueSelect.innerHTML = ''; if (!attrName) { var p = document.createElement('option'); p.value = ''; p.textContent = 'ابتدا ویژگی را انتخاب کنید'; valueSelect.appendChild(p); return; } var data = findAttribute(attrName); var first = document.createElement('option'); first.value = ''; first.textContent = 'انتخاب مقدار'; valueSelect.appendChild(first); var any = document.createElement('option'); any.value = '__any__'; any.textContent = 'همه موارد'; valueSelect.appendChild(any); if (data && data.options) { data.options.forEach(function(opt){ var option = document.createElement('option'); option.value = opt.value; option.textContent = opt.label; valueSelect.appendChild(option); }); } if (previousValue) { var exists = false; for (var i = 0; i < valueSelect.options.length; i++) { if (valueSelect.options[i].value === previousValue) { exists = true; break; } } valueSelect.value = exists ? previousValue : ''; } } attr1.addEventListener('change', function(e){ e.stopPropagation(); fillValues(attr1, val1); if (attr2.value && attr2.value === attr1.value) { attr2.value = ''; fillValues(attr2, val2); } }, true); attr2.addEventListener('change', function(e){ e.stopPropagation(); if (attr1.value && attr2.value && attr1.value === attr2.value) { alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.'); attr2.value = ''; } fillValues(attr2, val2); }, true); val1.addEventListener('change', function(e){ e.stopPropagation(); }, true); val2.addEventListener('change', function(e){ e.stopPropagation(); }, true); attr1.addEventListener('click', function(e){ e.stopPropagation(); }, true); attr2.addEventListener('click', function(e){ e.stopPropagation(); }, true); val1.addEventListener('click', function(e){ e.stopPropagation(); }, true); val2.addEventListener('click', function(e){ e.stopPropagation(); }, true); })(); </script> <?php } add_action('woocommerce_after_single_product_summary', 'qv_render_quick_variation_form', 5); /** * ثبت فرم */ function qv_handle_quick_variation_submit() { if (!isset($_POST['qv_quick_variation_submit'])) return; if (!qv_is_admin_user()) return; if (!isset($_POST['qv_quick_variation_nonce']) || !wp_verify_nonce($_POST['qv_quick_variation_nonce'], 'qv_quick_variation_action')) { return; } $product_id = isset($_POST['qv_product_id']) ? absint($_POST['qv_product_id']) : 0; $attr1 = isset($_POST['qv_attr1']) ? wc_clean(wp_unslash($_POST['qv_attr1'])) : ''; $val1 = isset($_POST['qv_val1']) ? wc_clean(wp_unslash($_POST['qv_val1'])) : ''; $attr2 = isset($_POST['qv_attr2']) ? wc_clean(wp_unslash($_POST['qv_attr2'])) : ''; $val2 = isset($_POST['qv_val2']) ? wc_clean(wp_unslash($_POST['qv_val2'])) : ''; $price = isset($_POST['qv_price']) ? wc_format_decimal(wp_unslash($_POST['qv_price'])) : ''; if (!$product_id || !$attr1 || $val1 === '' || $price === '') { wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error'); return; } if ($attr2 && !$val2 && $val2 !== '__any__') { wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error'); return; } if ($attr1 && $attr2 && $attr1 === $attr2) { wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error'); return; } if ($val1 === '__any__' && $val2 === '__any__') { wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان همه موارد را انتخاب کرد.', 'error'); return; } if (!qv_ensure_variable_product($product_id)) { wc_add_notice('تبدیل محصول به variable ناموفق بود.', 'error'); return; } qv_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : ''); if ($attr2) { qv_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : ''); } $variation_attributes = array( 'attribute_' . $attr1 => ($val1 === '__any__' ? '' : $val1), ); if ($attr2) { $variation_attributes['attribute_' . $attr2] = ($val2 === '__any__' ? '' : $val2); } if (qv_variation_exists($product_id, $variation_attributes)) { wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error'); return; } $variation_post = array( 'post_title' => 'Product variation', 'post_name' => 'product-' . $product_id . '-variation', 'post_status' => 'publish', 'post_parent' => $product_id, 'post_type' => 'product_variation', 'guid' => home_url('/?product_variation=product-' . $product_id . '-variation'), ); $variation_id = wp_insert_post($variation_post); if (!$variation_id || is_wp_error($variation_id)) { wc_add_notice('ساخت variation ناموفق بود.', 'error'); return; } foreach ($variation_attributes as $meta_key => $meta_value) { update_post_meta($variation_id, $meta_key, $meta_value); } update_post_meta($variation_id, '_regular_price', $price); update_post_meta($variation_id, '_price', $price); $variation = new WC_Product_Variation($variation_id); $variation->set_parent_id($product_id); $variation->set_regular_price($price); $variation->set_price($price); $set_attrs = array( $attr1 => ($val1 === '__any__' ? '' : $val1), ); if ($attr2) { $set_attrs[$attr2] = ($val2 === '__any__' ? '' : $val2); } $variation->set_attributes($set_attrs); $variation->save(); WC_Product_Variable::sync($product_id); wc_delete_product_transients($product_id); wc_add_notice('تنوع جدید با موفقیت ساخته شد.', 'success'); } add_action('init', 'qv_handle_quick_variation_submit'); add_action('template_redirect', 'fz_bed_vertical_price_date_start_buffer', 0); add_filter('pre_get_document_title', 'fz_bed_vertical_price_date_filter_text', 999999); add_filter('document_title_parts', 'fz_bed_vertical_price_date_document_parts', 999999); add_filter('rank_math/frontend/title', 'fz_bed_vertical_price_date_filter_text', 999999); add_filter('rank_math/opengraph/facebook/title', 'fz_bed_vertical_price_date_filter_text', 999999); add_filter('rank_math/opengraph/twitter/title', 'fz_bed_vertical_price_date_filter_text', 999999); add_filter('woocommerce_page_title', 'fz_bed_vertical_price_date_filter_text', 999999); add_filter('single_term_title', 'fz_bed_vertical_price_date_filter_text', 999999); function fz_bed_vertical_price_date_is_target() { if (is_admin()) { return false; } return function_exists('is_product_category') && is_product_category(); } function fz_bed_vertical_price_date_start_buffer() { if (!fz_bed_vertical_price_date_is_target()) { return; } ob_start('fz_bed_vertical_price_date_replace_html'); } function fz_bed_vertical_price_date_document_parts($parts) { if (!fz_bed_vertical_price_date_is_target()) { return $parts; } if (isset($parts['title'])) { $parts['title'] = fz_bed_vertical_price_date_filter_text($parts['title']); } return $parts; } function fz_bed_vertical_price_date_filter_text($text) { if (!fz_bed_vertical_price_date_is_target()) { return $text; } return fz_bed_vertical_price_date_add_suffix($text); } function fz_bed_vertical_price_date_replace_html($html) { if (!fz_bed_vertical_price_date_is_target()) { return $html; } $html = preg_replace_callback( '/<title\b[^>]*>(.*?)<\/title>/is', function ($m) { return '<title>' . esc_html(fz_bed_vertical_price_date_add_suffix($m[1])) . '</title>'; }, $html, 1 ); $html = preg_replace_callback( '/<h1\b([^>]*)>(.*?)<\/h1>/is', function ($m) { return '<h1' . $m[1] . '>' . esc_html(fz_bed_vertical_price_date_add_suffix(wp_strip_all_tags($m[2]))) . '</h1>'; }, $html, 1 ); $html = preg_replace_callback( '/<meta\b[^>]*>/is', function ($m) { $tag = $m[0]; if ( stripos($tag, 'og:title') === false && stripos($tag, 'twitter:title') === false ) { return $tag; } if (!preg_match('/content=(["\'])(.*?)\1/is', $tag, $cm)) { return $tag; } $new_content = esc_attr( fz_bed_vertical_price_date_add_suffix( wp_strip_all_tags($cm[2]) ) ); return preg_replace( '/content=(["\'])(.*?)\1/is', 'content="' . $new_content . '"', $tag, 1 ); }, $html ); return $html; } function fz_bed_vertical_price_date_add_suffix($text) { $text = trim(wp_strip_all_tags($text)); $suffix = ' + قیمت روز ' . fz_bed_vertical_price_date_today_jalali(); if (mb_strpos($text, $suffix) !== false) { return $text; } return $text . $suffix; } function fz_bed_vertical_price_date_today_jalali() { $timestamp = current_time('timestamp'); $gy = (int) date('Y', $timestamp); $gm = (int) date('n', $timestamp); $gd = (int) date('j', $timestamp); list($jy, $jm, $jd) = fz_bed_vertical_price_date_gregorian_to_jalali($gy, $gm, $gd); $months = array( 1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد', 4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور', 7 => 'مهر', 8 => 'آبان', 9 => 'آذر', 10 => 'دی', 11 => 'بهمن', 12 => 'اسفند', ); return fz_bed_vertical_price_date_fa_num($jd) . ' ' . $months[$jm]; } function fz_bed_vertical_price_date_fa_num($num) { return str_replace( array('0','1','2','3','4','5','6','7','8','9'), array('۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'), (string) $num ); } function fz_bed_vertical_price_date_gregorian_to_jalali($gy, $gm, $gd) { $g_d_m = array(0,31,59,90,120,151,181,212,243,273,304,334); if ($gy > 1600) { $jy = 979; $gy -= 1600; } else { $jy = 0; $gy -= 621; } $gy2 = ($gm > 2) ? ($gy + 1) : $gy; $days = 365 * $gy + intval(($gy2 + 3) / 4) - intval(($gy2 + 99) / 100) + intval(($gy2 + 399) / 400) - 80 + $gd + $g_d_m[$gm - 1]; $jy += 33 * intval($days / 12053); $days %= 12053; $jy += 4 * intval($days / 1461); $days %= 1461; if ($days > 365) { $jy += intval(($days - 1) / 365); $days = ($days - 1) % 365; } if ($days < 186) { $jm = 1 + intval($days / 31); $jd = 1 + ($days % 31); } else { $jm = 7 + intval(($days - 186) / 30); $jd = 1 + (($days - 186) % 30); } return array($jy, $jm, $jd); } /** * Front-end Price Editor (Simple + Variable) - Code Snippets * ✅ فقط قیمت عادی Regular * ✅ مناسب LiteSpeed Cache: بعد از تغییر قیمت، کش همان محصول پاک می‌شود */ if ( ! defined('ABSPATH') ) exit; /** Optional: remove "choose an option" placeholder in variation dropdowns */ add_filter('woocommerce_dropdown_variation_attribute_options_args', function($args){ $args['show_option_none'] = false; return $args; }); /** Convert Persian/Arabic digits to English + keep only digits */ function fpe_digits_only($val){ $val = (string) $val; $map = [ '۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9', '٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9', ]; $val = strtr($val, $map); return preg_replace('/\D+/', '', $val); } /** Purge product cache after price update */ function fpe_purge_product_cache($product_id){ $product_id = absint($product_id); if ( ! $product_id ) return; if ( function_exists('wc_delete_product_transients') ) { wc_delete_product_transients($product_id); } clean_post_cache($product_id); if ( function_exists('wc_get_product') ) { $product = wc_get_product($product_id); if ( $product && $product->is_type('variation') ) { $parent_id = $product->get_parent_id(); if ( $parent_id ) { wc_delete_product_transients($parent_id); clean_post_cache($parent_id); do_action('litespeed_purge_post', $parent_id); do_action('litespeed_purge_url', get_permalink($parent_id)); } } } do_action('litespeed_purge_post', $product_id); do_action('litespeed_purge_url', get_permalink($product_id)); } /** Variation label helpers */ function fpe_attribute_label($attr_key, $parent_product){ $key = preg_replace('/^attribute_/', '', (string)$attr_key); if (strpos($key, 'pa_') === 0 && taxonomy_exists($key)) { $tax = get_taxonomy($key); if ($tax && ! empty($tax->labels->singular_name)) return $tax->labels->singular_name; return wc_attribute_label($key, $parent_product); } $label = wc_attribute_label($key, $parent_product); if ($label && $label !== $key) return $label; return str_replace(['pa_', '-', '_'], ['', ' ', ' '], $key); } function fpe_attribute_value_readable($taxonomy_or_name, $raw_val){ $raw_val = (string)$raw_val; $decoded = rawurldecode($raw_val); $tax = preg_replace('/^attribute_/', '', (string)$taxonomy_or_name); if (strpos($tax, 'pa_') === 0 && taxonomy_exists($tax)) { $term = get_term_by('slug', $raw_val, $tax); if ( ! $term || is_wp_error($term) ) $term = get_term_by('slug', $decoded, $tax); if ( ! $term || is_wp_error($term) ) $term = get_term_by('name', $decoded, $tax); if ( $term && ! is_wp_error($term) ) return $term->name; return $decoded; } return $decoded; } function fpe_get_variation_label($variation, $parent_product){ $out = []; foreach ((array)$variation->get_attributes() as $k => $v) { if ($v === '' || $v === null) continue; $out[] = fpe_attribute_label($k, $parent_product) . ': ' . fpe_attribute_value_readable($k, $v); } return $out ? implode(' | ', $out) : ('تنوع #' . $variation->get_id()); } /** UI */ add_action('woocommerce_after_add_to_cart_form', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( ! is_user_logged_in() ) return; if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) return; global $product; if ( ! $product ) return; $type = $product->get_type(); if ( $type !== 'simple' && $type !== 'variable' ) return; $product_id = $product->get_id(); $nonce = wp_create_nonce('fpe_save_prices'); $title = ($type === 'simple') ? 'ویرایش قیمت محصول ساده' : 'ویرایش قیمت تنوع‌ها'; echo '<style> .fpe-wrap{margin:16px 0;} .fpe-details{border:1px solid #e5e7eb;border-radius:14px;background:#fafafa;overflow:hidden;} .fpe-details>summary{list-style:none;cursor:pointer;padding:12px;display:flex;align-items:center;gap:10px;user-select:none;} .fpe-details>summary::-webkit-details-marker{display:none;} .fpe-badge{font-size:12px;padding:4px 10px;border-radius:999px;background:#111;color:#fff;white-space:nowrap;} .fpe-title{font-size:14px;font-weight:900;line-height:1.4;margin:0;flex:1;} .fpe-hint{font-size:12px;opacity:.7;margin:0;} .fpe-body{padding:12px;} .fpe-grid-head{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:10px;border-bottom:1px solid #e5e7eb;font-size:13px;font-weight:900;background:#f3f4f6;border-radius:12px;margin-bottom:10px;} .fpe-row{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:12px 10px;border:1px solid #e5e7eb;border-radius:14px;align-items:center;margin-bottom:10px;background:#fff;} .fpe-rows .fpe-row:nth-child(even){ background:#eef6ff; } .fpe-attr{font-size:13px;line-height:1.6;word-break:break-word;font-weight:800;} .fpe-input{ width:100%;padding:10px;border:1px solid #c3c4c7;border-radius:10px;font-size:16px;outline:none;background:#fff; direction:ltr;text-align:center; } .fpe-input:focus{border-color:#2271b1; box-shadow:0 0 0 1px #2271b1;} .fpe-actions{margin-top:12px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;} .fpe-note{font-size:12px;opacity:.75;margin:0;} .fpe-btn{width:100%;padding:12px 18px;border:1px solid #2271b1;border-radius:6px;cursor:pointer;font-size:14px;font-weight:700;background:#2271b1;color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.08);} .fpe-btn:hover{background:#135e96;border-color:#135e96;} .fpe-btn:active{background:#0a4b78;border-color:#0a4b78;transform:translateY(1px);} @media (max-width:680px){ .fpe-grid-head{display:none;} .fpe-row{grid-template-columns:1fr;gap:10px;padding:12px;} .fpe-field{display:flex;flex-direction:column;gap:6px;} .fpe-label{font-size:12px;opacity:.75;} .fpe-attr{font-size:14px;} } @media (min-width:681px){ .fpe-btn{width:auto;min-width:220px;} .fpe-label{display:none;} .fpe-field{display:block;} } </style>'; echo '<div class="fpe-wrap">'; echo '<details class="fpe-details" '.(isset($_GET["fpe_saved"]) ? "open" : "").'>'; echo '<summary><span class="fpe-badge">مدیر</span> <div style="min-width:0;"> <p class="fpe-title">'.esc_html($title).'</p> <p class="fpe-hint">قیمت‌ها حین تایپ سه‌تایی جدا می‌شوند</p> </div> <span style="opacity:.65;font-size:18px;">⌄</span> </summary>'; echo '<div class="fpe-body"><form method="post" id="fpe-form">'; echo '<input type="hidden" name="fpe_product_id" value="'.esc_attr($product_id).'">'; echo '<input type="hidden" name="fpe_nonce" value="'.esc_attr($nonce).'">'; echo '<input type="hidden" name="fpe_type" value="'.esc_attr($type).'">'; echo '<div class="fpe-grid-head"><div>'.($type==='simple'?'محصول':'تنوع').'</div><div>قیمت</div></div>'; echo '<div class="fpe-rows">'; if ($type === 'simple') { $raw = fpe_digits_only($product->get_regular_price()); echo '<div class="fpe-row"> <div class="fpe-attr">این محصول</div> <div class="fpe-field"> <div class="fpe-label">قیمت</div> <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_simple_regular" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000"> </div> </div>'; } else { foreach ($product->get_children() as $variation_id) { $v = wc_get_product($variation_id); if (!$v) continue; $label = fpe_get_variation_label($v, $product); $raw = fpe_digits_only($v->get_regular_price()); echo '<div class="fpe-row"> <div class="fpe-attr">'.esc_html($label).'</div> <div class="fpe-field"> <div class="fpe-label">قیمت</div> <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_regular['.esc_attr($variation_id).']" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000"> </div> </div>'; } } echo '</div>'; echo '<div class="fpe-actions"> <button class="fpe-btn" type="submit" name="fpe_save" value="1">به‌روزرسانی</button> <p class="fpe-note">بعد از به‌روزرسانی، کش همان محصول پاک می‌شود.</p> </div>'; echo '</form></div></details></div>'; echo '<script> (function(){ function toEnDigits(s){ if(!s) return ""; var map = {"۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"}; return String(s).replace(/[۰-۹٠-٩]/g, function(ch){ return map[ch] || ch; }); } function digitsOnly(s){ return toEnDigits(s).replace(/\\D+/g,""); } function format3(s){ s = digitsOnly(s); if(!s) return ""; return s.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ","); } function caretDigitsIndex(value, caretPos){ var left = value.slice(0, caretPos); return digitsOnly(left).length; } function caretFromDigitsIndex(formatted, digitIndex){ var count = 0; for (var i=0; i<formatted.length; i++){ if (/\\d/.test(formatted[i])) count++; if (count >= digitIndex) return i+1; } return formatted.length; } var inputs = document.querySelectorAll(".fpe-wrap .fpe-price"); inputs.forEach(function(inp){ inp.value = format3(inp.value); inp.addEventListener("input", function(){ var oldVal = inp.value; var caret = inp.selectionStart || 0; var dIndex = caretDigitsIndex(oldVal, caret); var newVal = format3(oldVal); inp.value = newVal; var newCaret = caretFromDigitsIndex(newVal, dIndex); try { inp.setSelectionRange(newCaret, newCaret); } catch(err){} }); inp.addEventListener("paste", function(){ setTimeout(function(){ inp.value = format3(inp.value); }, 0); }); }); var form = document.getElementById("fpe-form"); if(form){ form.addEventListener("submit", function(){ inputs.forEach(function(inp){ inp.value = digitsOnly(inp.value); }); }); } })(); </script>'; }, 50); /** Save handler */ add_action('template_redirect', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( empty($_POST['fpe_save']) ) return; if ( ! is_user_logged_in() ) wp_die('Access denied'); if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) wp_die('Access denied'); $nonce = isset($_POST['fpe_nonce']) ? sanitize_text_field($_POST['fpe_nonce']) : ''; if ( ! wp_verify_nonce($nonce, 'fpe_save_prices') ) wp_die('Security check failed'); $product_id = isset($_POST['fpe_product_id']) ? absint($_POST['fpe_product_id']) : 0; if ( ! $product_id ) wp_die('Invalid product'); $product = wc_get_product($product_id); if ( ! $product ) wp_die('Product not found'); $type = isset($_POST['fpe_type']) ? sanitize_text_field($_POST['fpe_type']) : $product->get_type(); // SIMPLE if ( $type === 'simple' && $product->is_type('simple') ) { $new_raw = isset($_POST['fpe_simple_regular']) ? fpe_digits_only(wp_unslash($_POST['fpe_simple_regular'])) : ''; $old_raw = fpe_digits_only($product->get_regular_price()); if ($new_raw !== $old_raw) { $product->set_regular_price( $new_raw === '' ? '' : $new_raw ); $product->save(); fpe_purge_product_cache($product_id); } wp_safe_redirect( add_query_arg('fpe_saved', '1', get_permalink($product_id)) ); exit; } // VARIABLE if ( $type === 'variable' && $product->is_type('variable') ) { $regulars = (isset($_POST['fpe_regular']) && is_array($_POST['fpe_regular'])) ? $_POST['fpe_regular'] : []; $changed_any = false; foreach ( $product->get_children() as $variation_id ) { if ( ! array_key_exists($variation_id, $regulars) ) continue; $v = wc_get_product($variation_id); if (!$v) continue; $new_raw = fpe_digits_only( wp_unslash($regulars[$variation_id]) ); $old_raw = fpe_digits_only( $v->get_regular_price() ); if ($new_raw === $old_raw) continue; $v->set_regular_price( $new_raw === '' ? '' : $new_raw ); $v->save(); fpe_purge_product_cache($variation_id); $changed_any = true; } if ($changed_any) { fpe_purge_product_cache($product_id); } wp_safe_redirect( add_query_arg('fpe_saved', '1', get_permalink($product_id)) ); exit; } wp_die('Unsupported product type'); }); /** Toast */ add_action('wp_footer', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( isset($_GET['fpe_saved']) ) { echo '<div id="fpe-toast" style="position:fixed;bottom:18px;left:18px;z-index:999999;background:#111;color:#fff;padding:10px 12px;border-radius:14px;font-size:13px;">به‌روزرسانی انجام شد ✅</div>'; echo '<script>setTimeout(function(){var t=document.getElementById("fpe-toast"); if(t) t.remove();}, 3200);</script>'; } });
// نمایش "شماره سفارش | روش پرداخت" در باکس اطلاعات سفارش ادمین
add_action('woocommerce_admin_order_data_after_billing_address', function ( $order ) {
    if ( ! $order ) return;
    $order_no  = $order->get_order_number();
    $pay_title = $order->get_payment_method_title(); // نام نمایشی درگاه
    if ( empty($pay_title) ) { 
        $pay_title = $order->get_payment_method();   // اسلاگ درگاه، اگر نام نبود
    }
    echo '<p id="order-quick-info" style="font-weight:600;font-size:15px;direction:rtl;margin-top:8px;">
            شماره سفارش: ' . esc_html($order_no) . 
            ( $pay_title ? ' | روش پرداخت: ' . esc_html($pay_title) : '' ) .
          '</p>';
}, 20 );

/**
 * Admin order tweaks (WooCommerce):
 * - Hide only SMS-related lines (safe)
 * - Enlarge order-item thumbnails column
 * - Force high-quality image in admin (replace default tiny thumbnail)
 */

/* ==== CSS: عرض ستون تصویر + ایمنی ستون‌ها + تیتر دلخواه ==== */
add_action('admin_head', function () { ?>
  <style>
    /* عرض ستون تصویر آیتم سفارش (در صورت نیاز کمتر/بیشترش کن) */
    .woocommerce-page.post-type-shop_order .wc-order-items td.thumb,
    .woocommerce-page.post-type-shop_order .wc-order-items .wc-order-item-thumbnail,
    .wc-order-items .thumb,
    .wc-order-items td.product-thumbnail,
    .wc-order-item-thumbnail{
      width: 160px !important;
      min-width: 160px !important;
      max-width: none !important;
    }
    .woocommerce-page.post-type-shop_order .wc-order-items td.thumb img,
    .woocommerce-page.post-type-shop_order .wc-order-items .wc-order-item-thumbnail img,
    .wc-order-items .thumb img,
    .wc-order-item-thumbnail img{
      display: block !important;
      width: 100% !important;
      height: auto !important;
      max-width: none !important;
      object-fit: contain !important;
      image-rendering: auto;
    }

    /* مطمئن شو ستون‌های اطلاعات مشتری پنهان نشوند */
    .order_data_column{ display:block !important; }

    /* (اختیاری) ریز کردن تیتر سفارشی */
    #order-quick-info{ font-size:13px !important; font-weight:600; }
  </style>
<?php });

/* ==== JS: فقط خطوط مربوط به «پیامک/SMS» را مخفی کن (نه والدهای بزرگ) ==== */
add_action('admin_footer', function () { ?>
  <script>
  (function(){
    var inOrder = document.querySelector('.woocommerce-order-data, .wc-order-items');
    if(!inOrder) return;

    var TEXTS = [
      'آیا مشتری مایل به دریافت پیامک هست',
      'مشتری حق انتخاب وضعیت های دریافت پیامک را ندارد',
      'دریافت پیامک','SMS','sms'
    ];

    // اگر با لیبل مشخص است، همان فیلد را مخفی کن
    var label = document.querySelector('label[for="_billing_sms_consent"], label[for="billing_sms_consent"]');
    if (label) {
      var field = label.closest('.form-field, .options_group, p');
      if (field) field.style.display = 'none';
    }

    function hideSmsLines(ctx){
      (ctx||document).querySelectorAll('#order_data p, .order_data_column p, .postbox .inside p, .options_group .form-field, .woocommerce-order-data p')
      .forEach(function(el){
        var t = (el.innerText||'').replace(/\s+/g,' ').trim();
        if (!t) return;
        if (TEXTS.some(function(x){ return t.indexOf(x)!==-1; })) el.style.display='none';
      });
      document.querySelectorAll('.order_data_column').forEach(function(col){
        col.style.removeProperty('display'); col.hidden=false;
      });
    }
    hideSmsLines(document);
    new MutationObserver(function(){ hideSmsLines(document); }).observe(document.body,{childList:true,subtree:true});
  })();
  </script>
<?php });

/* ==== کیفیت بالا: جایگزینی تامب‌نیل کوچک با تصویر بزرگ/اصلی ==== */
/* این فیلتر، HTML تصویر آیتم را با سایز بزرگ‌تر بازتولید می‌کند. */
add_filter('woocommerce_admin_order_item_thumbnail', function($thumbnail, $item_id, $item){
    if ( ! is_admin() ) return $thumbnail;
    if ( ! $item || ! is_a($item, 'WC_Order_Item_Product') ) return $thumbnail;

    $product = $item->get_product();
    if ( ! $product ) return $thumbnail;

    $image_id = $product->get_image_id();
    if ( ! $image_id ) return $thumbnail;

    // 'large' معمولاً کافی و سبک است. اگر نهایت کیفیت می‌خواهی 'full' بگذار.
    $size = 'large'; // یا: 'woocommerce_single'  /  'full'
    $html = wp_get_attachment_image($image_id, $size, false, array(
        'loading' => 'eager',
        'decoding'=> 'async',
        'style'   => 'width:100%;height:auto;max-width:none;display:block'
    ));
    return $html ?: $thumbnail;
}, 10, 3);

/* (اختیاری) اگر نسخه‌های قدیمی از این فیلتر استفاده کنند، سایز را هم بزرگ‌تر اعلام کن */
add_filter('woocommerce_admin_order_item_thumbnail_size', function(){
    return 'large'; // در صورت نیاز 'full' یا 'woocommerce_single'
});

// اجرای JS فقط در ادمین برای تاگل مبلغ ⇄ "تسویه‌شده ✅"
add_action('admin_footer', function () { ?>
  <style>
    /* استایل حالت تسویه‌شده */
    .fz-paid-wrapper{display:inline-flex;flex-direction:column;align-items:flex-start;gap:4px;}
    .fz-paid-label{color:#16a34a;font-weight:800;font-size:18px;line-height:1.2;}
    .fz-paid-tick{color:#16a34a;font-size:22px;line-height:1;}
    .fz-amount{cursor:pointer;}
  </style>
  <script>
  (function(){
    function ready(fn){ if(document.readyState!=='loading') fn(); else document.addEventListener('DOMContentLoaded',fn); }
    ready(function(){
      // فقط صفحه ویرایش سفارش ووکامرس
      var isOrderEdit = document.body.classList.contains('post-type-shop_order') ||
                        document.querySelector('.woocommerce-order-data, .wc-order-items');
      if(!isOrderEdit) return;

      // تشخیص المان مبلغ
      function isAmount(el){
        return el && el.classList && (el.classList.contains('amount') || el.classList.contains('woocommerce-Price-amount'));
      }
      // برای کلیک‌پذیر شدن و ذخیره متن اصلی
      function prime(ctx){
        (ctx||document).querySelectorAll(
          '.wc-order-items .amount, .wc-order-totals .amount,'+
          '.wc-order-items .woocommerce-Price-amount, .wc-order-totals .woocommerce-Price-amount'
        ).forEach(function(el){
          if (!el.dataset.fzOriginal) el.dataset.fzOriginal = el.innerHTML;
          el.classList.add('fz-amount'); el.style.cursor='pointer';
        });
      }
      // رفت و برگشت بین مبلغ و "تسویه‌شده"
      function toggle(el){
        if(!el.closest('.wc-order-items, .wc-order-totals')) return; // فقط جدول آیتم‌ها/جمع‌کل
        if(el.classList.contains('fz-paid')){
          el.innerHTML = el.dataset.fzOriginal || el.innerHTML;
          el.classList.remove('fz-paid');
        }else{
          el.innerHTML =
            '<span class="fz-paid-wrapper">'+
              '<span class="fz-paid-label">تسویه\u200cشده</span>'+
              '<span class="fz-paid-tick">✅</span>'+
            '</span>';
          el.classList.add('fz-paid');
        }
      }

      // Event Delegation تا با Ajax/HPOS هم کار کند
      document.addEventListener('click', function(e){
        var el = e.target.closest('.amount, .woocommerce-Price-amount');
        if(!el) return;
        if(!el.closest('.wc-order-items, .wc-order-totals')) return;
        e.preventDefault();
        toggle(el);
      }, true);

      // آماده‌سازی اولیه و پس از تغییرات Ajax
      prime(document);
      new MutationObserver(function(m){ m.forEach(function(mu){
        (mu.addedNodes||[]).forEach(function(n){
          if(n.nodeType!==1) return;
          if(n.matches && (n.matches('.amount')||n.matches('.woocommerce-Price-amount'))) prime(n);
          else prime(n);
        });
      }); }).observe(document.body,{childList:true,subtree:true});
    });
  })();
  </script>
<?php });

/* مخفی کردن همه دکمه‌های خرید و بیعانه در کارت محصولات (فروشگاه و دسته‌ها) */
.archive.woocommerce .product-small a.button,
.archive.woocommerce .product-small button.button,
.archive.woocommerce .product-small .yith-wcdp {
    display: none !important;
}

/*************
 * آماده تحویل – متاباکس + نمایش در محصول و لیست
 *************/

/*-----------------------------
  متاباکس در صفحه محصول
-----------------------------*/
add_action( 'add_meta_boxes', 'fzd_ready_add_metabox' );
function fzd_ready_add_metabox() {
    add_meta_box(
        'fzd_ready_box',
        'آماده تحویل',
        'fzd_ready_metabox_callback',
        'product',
        'side',
        'high'
    );
}

function fzd_ready_metabox_callback( $post ) {
    $rows = get_post_meta( $post->ID, '_fzd_ready_rows', true );
if ( ! is_array( $rows ) || empty( $rows ) ) {
    // فقط یک ردیف خالی، بدون مقدار پیش‌فرض
    $rows = array(
        array( 'color' => '', 'days' => '' ),
    );
}

    $note = get_post_meta( $post->ID, '_fzd_ready_note', true );

    wp_nonce_field( 'fzd_ready_save', 'fzd_ready_nonce' );

    echo '<p>برای هر رنگ آماده تحویل، یک ردیف وارد کن.</p>';
    echo '<div id="fzd-ready-rows">';

    foreach ( $rows as $row ) {
        $color = isset( $row['color'] ) ? $row['color'] : '';
        $days  = isset( $row['days'] )  ? (int) $row['days'] : 3;

        echo '<div class="fzd-ready-row" style="margin-bottom:6px;border-bottom:1px solid #ddd;padding-bottom:6px;">';
        echo '<input type="text" name="fzd_ready_color[]" value="' . esc_attr( $color ) . '" placeholder="رنگ (مثلاً خودرنگ)" style="width:100%;margin-bottom:4px;">';
        echo '<input type="number" name="fzd_ready_days[]" value="' . esc_attr( $days ) . '" min="0" max="365" style="width:100%;margin-bottom:4px;" placeholder="روز تحویل">';
        echo '<button type="button" class="button fzd-ready-remove">حذف</button>';
        echo '</div>';
    }

    echo '</div>';
    echo '<button type="button" class="button button-secondary" id="fzd-ready-add">+ افزودن رنگ دیگر</button>';

    echo '<hr><p><strong>توضیح اضافه (اختیاری):</strong><br><small>این متن به رنگ سبز، زیر توضیحات آماده تحویل در صفحه محصول و دسته‌بندی نمایش داده می‌شود.</small></p>';
    echo '<textarea name="fzd_ready_note" style="width:100%;min-height:70px;">' . esc_textarea( $note ) . '</textarea>';

    ?>
    <script>
    (function($){
        $(function(){
            var $wrap = $('#fzd-ready-rows');
            $('#fzd-ready-add').on('click', function(e){
                e.preventDefault();
                var $first = $wrap.find('.fzd-ready-row:first').clone();
                $first.find('input').val('');
                $wrap.append($first);
            });
            $wrap.on('click', '.fzd-ready-remove', function(e){
                e.preventDefault();
                if ($wrap.find('.fzd-ready-row').length > 1) {
                    $(this).closest('.fzd-ready-row').remove();
                } else {
                    $(this).closest('.fzd-ready-row').find('input').val('');
                }
            });
        });
    })(jQuery);
    </script>
    <?php
}

add_action( 'save_post_product', 'fzd_ready_save_metabox' );
function fzd_ready_save_metabox( $post_id ) {
    if ( ! isset( $_POST['fzd_ready_nonce'] ) ||
         ! wp_verify_nonce( $_POST['fzd_ready_nonce'], 'fzd_ready_save' ) ) return;
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;

    // رنگ‌ها + روزها
    if ( isset( $_POST['fzd_ready_color'], $_POST['fzd_ready_days'] ) ) {
        $colors = (array) $_POST['fzd_ready_color'];
        $days   = (array) $_POST['fzd_ready_days'];

        $rows = array();
        foreach ( $colors as $i => $color ) {
            $color = sanitize_text_field( wp_unslash( $color ) );
            $d     = isset( $days[ $i ] ) ? (int) $days[ $i ] : 0;

            if ( $color === '' ) continue;
            if ( $d < 1 ) $d = 1;

            $rows[] = array(
                'color' => $color,
                'days'  => $d,
            );
        }

        if ( ! empty( $rows ) ) {
            update_post_meta( $post_id, '_fzd_ready_rows', $rows );
        } else {
            delete_post_meta( $post_id, '_fzd_ready_rows' );
        }
    }// توضیح دستی
    if ( isset( $_POST['fzd_ready_note'] ) ) {
        $note = sanitize_textarea_field( wp_unslash( $_POST['fzd_ready_note'] ) );
        if ( $note !== '' ) {
            update_post_meta( $post_id, '_fzd_ready_note', $note );
        } else {
            delete_post_meta( $post_id, '_fzd_ready_note' );
        }
    }
}

/*-----------------------------
  توابع کمکی (شمسی + ارقام فارسی)
-----------------------------*/
function fzd_ready_get_rows( $product_id ) {
    $rows = get_post_meta( $product_id, '_fzd_ready_rows', true );
    return is_array( $rows ) ? $rows : array();
}

function fzd_ready_get_note( $product_id ) {
    $note = get_post_meta( $product_id, '_fzd_ready_note', true );
    return trim( (string) $note );
}

function fzd_ready_persian_digits( $str ) {
    $en = array('0','1','2','3','4','5','6','7','8','9');
    $fa = array('۰','۱','۲','۳','۴','۵','۶','۷','۸','۹');
    return str_replace( $en, $fa, (string) $str );
}

function fzd_ready_convert_jalali( $timestamp ) {
    $gy = (int) gmdate( 'Y', $timestamp );
    $gm = (int) gmdate( 'n', $timestamp );
    $gd = (int) gmdate( 'j', $timestamp );

    $g_d_m = array(0,31,59,90,120,151,181,212,243,273,304,334);

    if ( $gy > 1600 ) { $jy = 979; $gy -= 1600; }
    else { $jy = 0; $gy -= 621; }

    $gy2 = ( $gm > 2 ) ? ( $gy + 1 ) : $gy;
    $days = 365*$gy + (int)(($gy2+3)/4) - (int)(($gy2+99)/100) + (int)(($gy2+399)/400) - 80 + $gd + $g_d_m[$gm-1];

    $jy += 33*(int)($days/12053); $days %= 12053;
    $jy += 4*(int)($days/1461);   $days %= 1461;

    if ( $days > 365 ) { $jy += (int)(($days-1)/365); $days = ($days-1)%365; }

    if ( $days < 186 ) { $jm = 1 + (int)($days/31); $jd = 1 + ($days%31); }
    else { $jm = 7 + (int)(($days-186)/30); $jd = 1 + (($days-186)%30); }

    $months = array(
        1=>'فروردین',2=>'اردیبهشت',3=>'خرداد',4=>'تیر',5=>'مرداد',6=>'شهریور',
        7=>'مهر',8=>'آبان',9=>'آذر',10=>'دی',11=>'بهمن',12=>'اسفند',
    );

    $day_fa   = fzd_ready_persian_digits( $jd );
    $month_fa = isset( $months[$jm] ) ? $months[$jm] : '';

    return $day_fa . ' ' . $month_fa;
}

/*-----------------------------
  نمایش داخل صفحه محصول
-----------------------------*/
add_action( 'woocommerce_single_product_summary', 'fzd_ready_single_box', 12 );
function fzd_ready_single_box() {
    if ( ! is_product() ) return;

    $product_id = get_the_ID();
    if ( ! $product_id ) return;

    $product = wc_get_product( $product_id );
    if ( ! $product || ! $product->is_in_stock() ) return;

    $rows = fzd_ready_get_rows( $product_id );
    if ( empty( $rows ) ) return;

    echo '<div style="margin-top:10px;margin-bottom:10px;padding:8px 12px;border:1px solid #e53935;border-radius:6px;font-size:16px;line-height:1.9;">';
    echo '<strong>رنگ‌های آماده تحویل:</strong>';
    echo '<ul style="margin:5px 0 0 0;padding-right:18px;list-style:disc;">';

    foreach ( $rows as $row ) {
        $color = isset( $row['color'] ) ? $row['color'] : '';
        $days  = isset( $row['days'] )  ? (int) $row['days'] : 1;
        if ( $color === '' ) continue;
        if ( $days < 1 ) $days = 1;

        $ts   = current_time( 'timestamp' ) + $days * DAY_IN_SECONDS;
        $date = fzd_ready_convert_jalali( $ts );
        $days_fa = fzd_ready_persian_digits( $days );

        echo '<li>این محصول را در رنگ <strong>' . esc_html( $color ) .
             '</strong> تا <strong>' . esc_html( $date ) .
             '</strong> تحویل بگیرید (حدود ' . $days_fa . ' روزه).</li>';
    }

    echo '</ul>';

    // متن پیش‌فرض – مشکی
    $default_note = 'سایر رنگ‌ها به صورت سفارشی تولید می‌شوند و زمان تحویل آن‌ها کمی بیشتر است؛ پس از ثبت سفارش، زمان دقیق با شما هماهنگ می‌شود.';
    echo '<p style="margin-top:8px;font-size:14px;color:#333333;">' . esc_html( $default_note ) . '</p>';

    // توضیح دستی – سبز
    $note = fzd_ready_get_note( $product_id );
    if ( $note !== '' ) {
        echo '<p style="margin-top:2px;font-size:16px;color:#388e3c;">' . esc_html( $note ) . '</p>';
    }

    echo '</div>';
}/*-----------------------------
  لیبل «آماده تحویل» روی عکس (فلت‌سام)
-----------------------------*/
add_action( 'flatsome_woocommerce_shop_loop_images', 'fzd_ready_badge', 20 );
function fzd_ready_badge() {
    global $product;
    if ( ! $product || ! is_a( $product, 'WC_Product' ) ) return;
    if ( ! $product->is_in_stock() ) return;

    $rows = fzd_ready_get_rows( $product->get_id() );
    if ( empty( $rows ) ) return;

    // استایل فقط یک بار چاپ شود
    static $printed = false;
    if ( ! $printed ) {
    echo '<style>
        .product-small .box-image { position: relative; }

        /* لیبل آماده تحویل – گوشه بالا راست */
        .product-small .box-image .fzd-ready-badge {
            position: absolute;
            top: -2px;
            right: 8px;
            z-index: 10;
        }

        /* بادج تخفیف فلت‌سام – بیاد گوشه بالا چپ */
        .product-small .box-image .badge-container {
            left: 8px;
            right: auto;
        }
    </style>';
    $printed = true;
}

    echo '<span class="fzd-ready-badge" style="display:inline-block;background:#e53935;color:#ffffff;padding:3px 10px;border-radius:16px;font-size:14px;">آماده تحویل</span>';
}

/*-----------------------------
  متن تحویل + توضیح سبز در لیست محصولات
-----------------------------*/
add_action( 'woocommerce_after_shop_loop_item_title', 'fzd_ready_loop_text', 15 );
function fzd_ready_loop_text() {
    global $product;
    if ( ! $product || ! is_a( $product, 'WC_Product' ) ) return;
    if ( ! $product->is_in_stock() ) return;

    $rows = fzd_ready_get_rows( $product->get_id() );
    if ( empty( $rows ) ) return;

    $max   = 2; // حداکثر دو رنگ در دسته‌بندی
    $count = 0;

    // متن قرمز زیر محصول
    echo '<div style="margin-top:4px;font-size:14px;color:#c62828;line-height:1.7;">';

    foreach ( $rows as $row ) {
        if ( $count >= $max ) break;

        $color = isset( $row['color'] ) ? $row['color'] : '';
        $days  = isset( $row['days'] )  ? (int) $row['days'] : 1;
        if ( $color === '' ) continue;
        if ( $days < 1 ) $days = 1;

        $ts   = current_time( 'timestamp' ) + $days * DAY_IN_SECONDS;
        $date = fzd_ready_convert_jalali( $ts );

        echo 'رنگ ' . esc_html( $color ) . ' را تا ' . esc_html( $date ) . ' تحویل بگیرید<br>';

        $count++;
    }

    echo '</div>';

    // توضیح دستی سبز
    $note = fzd_ready_get_note( $product->get_id() );
    if ( $note !== '' ) {
        echo '<div style="margin-top:2px;font-size:14px;color:#388e3c;line-height:1.6;">' . esc_html( $note ) . '</div>';
    }
}

/* ----------------------------------------------------
 * 1) فیلد «تعداد در تخفیف» برای محصول ساده
 * --------------------------------------------------*/
add_action( 'woocommerce_product_options_pricing', 'my_add_promo_limit_field_simple' );
function my_add_promo_limit_field_simple() {

    woocommerce_wp_text_input( array(
        'id'                => '_promo_limit',
        'label'             => 'تعداد در تخفیف',
        'type'              => 'number',
        'desc_tip'          => true,
        'description'       => 'تعداد کل آیتم‌هایی که با قیمت حراج فروخته می‌شوند (در کل). اگر خالی یا 0 باشد، محدودیتی اعمال نمی‌شود.',
        'custom_attributes' => array(
            'min'  => '0',
            'step' => '1',
        ),
    ) );
}

add_action( 'woocommerce_admin_process_product_object', 'my_save_promo_limit_field_simple' );
function my_save_promo_limit_field_simple( $product ) {
    if ( isset( $_POST['_promo_limit'] ) ) {
        $promo_limit = max( 0, intval( $_POST['_promo_limit'] ) );
        $product->update_meta_data( '_promo_limit', $promo_limit );
    }
}

/* ----------------------------------------------------
 * 2) فیلد «تعداد در تخفیف» برای هر ورییشن
 * --------------------------------------------------*/
add_action( 'woocommerce_product_after_variable_attributes', 'my_add_variation_promo_limit_field', 10, 3 );
function my_add_variation_promo_limit_field( $loop, $variation_data, $variation ) {

    woocommerce_wp_text_input( array(
        'id'                => "variable_promo_limit_{$loop}",
        'name'              => "variable_promo_limit[{$loop}]",
        'value'             => get_post_meta( $variation->ID, '_promo_limit', true ),
        'label'             => 'تعداد در تخفیف',
        'type'              => 'number',
        'desc_tip'          => true,
        'description'       => 'تعداد کل این ورییشن که با قیمت حراج فروخته می‌شود (در کل).',
        'custom_attributes' => array(
            'min'  => '0',
            'step' => '1',
        ),
    ) );
}

add_action( 'woocommerce_save_product_variation', 'my_save_variation_promo_limit_field', 10, 2 );
function my_save_variation_promo_limit_field( $variation_id, $i ) {
    if ( isset( $_POST['variable_promo_limit'][ $i ] ) ) {
        $promo_limit = max( 0, intval( $_POST['variable_promo_limit'][ $i ] ) );
        update_post_meta( $variation_id, '_promo_limit', $promo_limit );
    }
}

/* ----------------------------------------------------
 * کمک‌تابع: متن توضیح تخفیف برای یک محصول/ورییشن
 * --------------------------------------------------*/
function my_get_promo_message_for_product( $pid ) {
    $promo_limit = intval( get_post_meta( $pid, '_promo_limit', true ) );
    if ( $promo_limit <= 0 ) {
        return '';
    }

    $sold_so_far = intval( get_option( 'promo_sold_' . $pid, 0 ) );
    $remaining   = max( 0, $promo_limit - $sold_so_far );

    if ( $remaining <= 0 ) {
        return 'تخفیف این محصول به پایان رسیده و از این پس با قیمت عادی و زمان تحویل پیش‌فرض ارسال می‌شود.';
    }

    if ( $remaining == 1 ) {
        return 'از این محصول با این تخفیف فقط ۱ عدد دیگر موجود است؛ تعداد بیشتر با قیمت عادی و زمان تحویل پیش‌فرض ارسال می‌شود.';
    }

    return 'از این محصول با این تخفیف فقط ' . $remaining . ' عدد دیگر موجود است؛ تعداد بیشتر با قیمت عادی و زمان تحویل پیش‌فرض ارسال می‌شود.';
}

/* ----------------------------------------------------
 * 3) اعمال تخفیف در سبد بر اساس محدودیت کلی هر محصول/ورییشن
 * --------------------------------------------------*/
add_action( 'woocommerce_before_calculate_totals', 'my_limit_discount_per_product_or_variation', 20, 1 );
function my_limit_discount_per_product_or_variation( $cart ) {

    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) return;
    if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 ) return;
    if ( $cart->is_empty() ) return;

    // گروه‌بندی آیتم‌ها بر اساس ID محصول/ورییشن
    $items_by_pid = array();

    foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {

        $product = $cart_item['data'];
        if ( ! $product ) continue;$pid = $product->get_id(); // برای ورییشن، ID خود ورییشن

        if ( ! isset( $items_by_pid[ $pid ] ) ) {
            $items_by_pid[ $pid ] = array();
        }

        $items_by_pid[ $pid ][] = $cart_item_key;
    }

    // اعمال تخفیف برای هر محصول/ورییشن با سقف کلی
    foreach ( $items_by_pid as $pid => $cart_item_keys ) {

        $promo_limit = intval( get_post_meta( $pid, '_promo_limit', true ) );
        if ( $promo_limit <= 0 ) {
            continue; // محدودیتی تعریف نشده
        }

        $sold_so_far = intval( get_option( 'promo_sold_' . $pid, 0 ) );
        $remaining   = max( 0, $promo_limit - $sold_so_far );

        // اگر چیزی باقی نمانده، همه با قیمت عادی
        if ( $remaining <= 0 ) {
            foreach ( $cart_item_keys as $cart_item_key ) {
                $cart_items = $cart->get_cart();
                if ( ! isset( $cart_items[ $cart_item_key ] ) ) continue;

                $cart_item = $cart_items[ $cart_item_key ];
                $product   = $cart_item['data'];
                $regular   = (float) $product->get_regular_price();
                if ( $regular ) {
                    $product->set_price( $regular );
                }
            }
            continue;
        }

        // روی تک‌تک خطوط برای این محصول/ورییشن
        foreach ( $cart_item_keys as $cart_item_key ) {

            $cart_items = $cart->get_cart();
            if ( ! isset( $cart_items[ $cart_item_key ] ) ) continue;

            $cart_item = $cart_items[ $cart_item_key ];
            $product   = $cart_item['data'];
            $qty       = $cart_item['quantity'];

            $regular_price = (float) $product->get_regular_price();
            $sale_price    = (float) $product->get_sale_price();

            if ( ! $regular_price || ! $sale_price ) {
                continue; // قیمت حراج تنظیم نشده
            }

            if ( $remaining <= 0 ) {
                $product->set_price( $regular_price );
                continue;
            }

            // تعداد تخفیف‌دار در این خط
            $discount_qty = min( $remaining, $qty );

            // جمع این خط: discount_qty با حراج، بقیه با قیمت عادی
            $line_total = $discount_qty * $sale_price + ( $qty - $discount_qty ) * $regular_price;

            // قیمت واحد میانگین، تا جمع درست دربیاید
            $product->set_price( $line_total / $qty );

            // از سقف باقی‌مانده کم کن
            $remaining -= $discount_qty;
        }
    }
}

/* ----------------------------------------------------
 * 4) به‌روزرسانی تعداد فروخته‌شده‌ی تخفیفی بعد از تغییر وضعیت سفارش
 *     (processing / completed / on-hold)
 * --------------------------------------------------*/
add_action( 'woocommerce_order_status_changed', 'my_update_promo_sold_qty_for_items', 10, 4 );
function my_update_promo_sold_qty_for_items( $order_id, $old_status, $new_status, $order ) {

    // فقط وقتی سفارش می‌ره روی وضعیت‌های مهم
    $target_statuses = array( 'processing', 'completed', 'on-hold' );
    if ( ! in_array( $new_status, $target_statuses, true ) ) {
        return;
    }

    // نذاریم یک سفارش دوبار حساب شود
    if ( 'yes' === get_post_meta( $order_id, '_promo_sold_counted', true ) ) {
        return;
    }
    update_post_meta( $order_id, '_promo_sold_counted', 'yes' );

    if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
        $order = wc_get_order( $order_id );
        if ( ! $order ) {
            return;
        }
    }

    foreach ( $order->get_items() as $item ) {

        $product = $item->get_product();
        if ( ! $product ) continue;

        $pid         = $product->get_id();
        $promo_limit = intval( get_post_meta( $pid, '_promo_limit', true ) );

        if ( $promo_limit <= 0 ) {
            continue; // برای این آیتم محدودیت تعریف نشده
        }

        $sold_so_far = intval( get_option( 'promo_sold_' . $pid, 0 ) );
        $remaining   = max( 0, $promo_limit - $sold_so_far );
        if ( $remaining <= 0 ) {
            continue;
        }$qty          = $item->get_quantity();
        $discount_qty = min( $remaining, $qty );

        // فقط همین مقدار را به‌عنوان تخفیفی ثبت می‌کنیم
        $sold_so_far += $discount_qty;
        update_option( 'promo_sold_' . $pid, $sold_so_far );

        // اگر سقف پر شد، قیمت حراج را از خود محصول/ورییشن بردار
        if ( $sold_so_far >= $promo_limit ) {
            $regular = get_post_meta( $pid, '_regular_price', true );
            update_post_meta( $pid, '_sale_price', '' );
            update_post_meta( $pid, '_price', $regular );
        }
    }
}

/* ----------------------------------------------------
 * 5) نمایش پیام زیر اسم محصول در سبد خرید
 * --------------------------------------------------*/
add_filter( 'woocommerce_cart_item_name', 'my_show_promo_limit_message_cart', 10, 3 );
function my_show_promo_limit_message_cart( $product_name, $cart_item, $cart_item_key ) {

    $product = isset( $cart_item['data'] ) ? $cart_item['data'] : false;
    if ( ! $product ) {
        return $product_name;
    }

    $pid         = $product->get_id();
    $promo_limit = intval( get_post_meta( $pid, '_promo_limit', true ) );
    if ( $promo_limit <= 0 ) {
        return $product_name; // محدودیت تعریف نشده
    }

    if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
        return $product_name;
    }

    $sold_so_far = intval( get_option( 'promo_sold_' . $pid, 0 ) );
    $remaining   = max( 0, $promo_limit - $sold_so_far );

    $cart        = WC()->cart;
    $total_qty   = 0;

    // مجموع تعداد این محصول/ورییشن در کل سبد
    foreach ( $cart->get_cart() as $ci ) {
        $p = isset( $ci['data'] ) ? $ci['data'] : false;
        if ( ! $p ) continue;
        if ( $p->get_id() == $pid ) {
            $total_qty += $ci['quantity'];
        }
    }

    if ( $total_qty <= 0 ) {
        return $product_name;
    }

    $discountable_in_cart = min( $remaining, $total_qty );

    if ( $remaining <= 0 ) {
        $msg = 'تخفیف این محصول تمام شده و همهٔ تعداد با قیمت عادی محاسبه می‌شوند.';
    } elseif ( $total_qty <= $discountable_in_cart ) {
        $msg = 'تا ' . $discountable_in_cart . ' عدد از این محصول با قیمت تخفیف محاسبه می‌شود.';
    } else {
        $nondiscounted = $total_qty - $discountable_in_cart;
        $msg = 'در این سبد فقط ' . $discountable_in_cart . ' عدد از این محصول با قیمت تخفیف محاسبه می‌شود و ' . $nondiscounted . ' عدد بعدی با قیمت عادی و زمان تحویل پیش‌فرض هستند.';
    }

    return $product_name . '<div class="promo-limit-msg" style="font-size:12px; color:#d33; margin-top:3px;">' . esc_html( $msg ) . '</div>';
}

/* ----------------------------------------------------
 * 6) نمایش پیام روی صفحه محصول
 *    - ساده: زیر قیمت
 *    - متغیر: کنار متن موجودی ورییشن
 * --------------------------------------------------*/

// محصول ساده: نمایش زیر قیمت
add_action( 'woocommerce_single_product_summary', 'my_show_promo_msg_on_single_simple', 11 );
function my_show_promo_msg_on_single_simple() {
    global $product;
    if ( ! $product ) return;

    if ( $product->is_type( 'simple' ) ) {
        $msg = my_get_promo_message_for_product( $product->get_id() );
        if ( $msg ) {
            echo '<div class="promo-msg-single" style="font-size:13px; color:#d33; margin-top:5px;">' . esc_html( $msg ) . '</div>';
        }
    }
}

// محصول متغیر: اضافه کردن پیام به availability_html هر ورییشن
add_filter( 'woocommerce_available_variation', 'my_add_promo_msg_to_variation_data', 10, 3 );
function my_add_promo_msg_to_variation_data( $data, $product, $variation ) {
    $msg = my_get_promo_message_for_product( $variation->get_id() );
    if ( $msg ) {

        if ( ! empty( $data['availability_html'] ) ) {
            $data['availability_html'] .= '<br><span class="promo-msg-single" style="color:#d33; font-size:13px;">' . esc_html( $msg ) . '</span>';
        } else {
            $data['availability_html'] = '<p class="stock promo-msg-single" style="color:#d33; font-size:13px;">' . esc_html( $msg ) . '</p>';
        }
    }
    return $data;
}

/* نمایش تعداد باقی‌مانده در تخفیف روی صفحه محصول / ورییشن در ادمین */

/* محصول ساده: زیر فیلد تعداد در تخفیف */
add_action( 'woocommerce_product_options_pricing', function () {

    global $post;
    if ( ! $post ) return;

    $product_id  = $post->ID;
    $promo_limit = intval( get_post_meta( $product_id, '_promo_limit', true ) );
    if ( $promo_limit <= 0 ) return;

    $sold_so_far = intval( get_option( 'promo_sold_' . $product_id, 0 ) );
    $remaining   = max( 0, $promo_limit - $sold_so_far );

    echo '<p style="margin-top:-8px; color:#0073aa; font-size:12px;">'
       . 'باقی‌مانده در تخفیف: <strong>' . $remaining . '</strong> عدد'
       . '</p>';
} );

/* محصول متغیر: برای هر ورییشن کنار فیلد تعداد در تخفیف */
add_action( 'woocommerce_product_after_variable_attributes', function( $loop, $variation_data, $variation ) {

    $vid         = $variation->ID; // ID خود ورییشن
    $promo_limit = intval( get_post_meta( $vid, '_promo_limit', true ) );
    if ( $promo_limit <= 0 ) return;

    $sold_so_far = intval( get_option( 'promo_sold_' . $vid, 0 ) );
    $remaining   = max( 0, $promo_limit - $sold_so_far );

    echo '<p style="margin:3px 0 0; color:#0073aa; font-size:12px;">'
       . 'باقی‌مانده در تخفیف برای این تنوع: <strong>' . $remaining . '</strong> عدد'
       . '</p>';

}, 20, 3 );

/* ==========
   Default sort for Shop + Categories = Best sellers (last 90 days)
   Label shown to user = "پرفروش‌ترین‌ها"
   Also calculates _sales_90d daily + manual recalc link
   ========== */

define('FARYAZAN_SALES_META', '_sales_90d');
define('FARYAZAN_ORDERBY_KEY', 'sales_90d');

/* 1) Schedule daily update */
add_action('init', function () {
    if (!wp_next_scheduled('faryazan_update_90d_metrics')) {
        wp_schedule_event(time() + 300, 'daily', 'faryazan_update_90d_metrics');
    }
});

/* 2) Calculate sales in last 90 days and store in _sales_90d */
add_action('faryazan_update_90d_metrics', function () {
    if (!function_exists('wc_get_orders') || !function_exists('wc_get_products')) return;

    $after_ts = time() - (90 * DAY_IN_SECONDS);
    $after    = gmdate('Y-m-d H:i:s', $after_ts);

    $sales = [];
    $page = 1;
    $per_page = 100;

    do {
        $orders = wc_get_orders([
            'status'       => ['processing', 'completed'],
            'limit'        => $per_page,
            'paged'        => $page,
            'date_created' => '>' . $after,
            'return'       => 'objects',
        ]);

        foreach ($orders as $order) {
            foreach ($order->get_items('line_item') as $item) {
                $pid = (int) $item->get_product_id();
                if ($pid <= 0) continue;

                $qty = (int) $item->get_quantity();
                if ($qty <= 0) continue;

                $sales[$pid] = ($sales[$pid] ?? 0) + $qty;
            }
        }

        $page++;
    } while (!empty($orders));

    $product_ids = wc_get_products(['return' => 'ids', 'limit' => -1]);
    foreach ($product_ids as $pid) {
        $s90 = (int) ($sales[$pid] ?? 0);
        update_post_meta($pid, FARYAZAN_SALES_META, $s90);
    }
});

/* 3) Manual recalculation (admin only):
      https://YOURDOMAIN.COM/?faryazan_recalc=1
*/
add_action('init', function () {
    if (!is_user_logged_in() || !current_user_can('manage_woocommerce')) return;

    if (isset($_GET['faryazan_recalc']) && $_GET['faryazan_recalc'] === '1') {
        do_action('faryazan_update_90d_metrics');
        wp_die('OK ✅ 90-day sales recalculated. You can close this page.');
    }
});

/* 4) Rename the option shown to user -> "پرفروش‌ترین‌ها" */
add_filter('woocommerce_catalog_orderby', function ($options) {
    $options[FARYAZAN_ORDERBY_KEY] = 'پرفروش‌ترین‌ها';
    return $options;
}, 20);

add_filter('woocommerce_default_catalog_orderby_options', function ($options) {
    $options[FARYAZAN_ORDERBY_KEY] = 'پرفروش‌ترین‌ها';
    return $options;
}, 20);

/* 5) Make our option the DEFAULT for all shop/category pages */
add_filter('woocommerce_default_catalog_orderby', function ($default) {
    return FARYAZAN_ORDERBY_KEY;
}, 20);

/* 6) Apply ordering ONLY when that option is used (which is now default too) */
add_filter('woocommerce_get_catalog_ordering_args', function ($args, $orderby, $order) {
    if ($orderby === FARYAZAN_ORDERBY_KEY) {
        $args['orderby']  = 'meta_value_num';
        $args['order']    = 'DESC';
        $args['meta_key'] = FARYAZAN_SALES_META;

        // Keep products visible even if meta missing
        $args['meta_query'] = [
            'relation' => 'OR',
            [
                'key'     => FARYAZAN_SALES_META,
                'compare' => 'EXISTS',
                'type'    => 'NUMERIC',
            ],
            [
                'key'     => FARYAZAN_SALES_META,
                'compare' => 'NOT EXISTS',
            ],
        ];
    }
    return $args;
}, 20, 3);


// جستجو در محصولات ووکامرس بر اساس SKU (سازگار با AJAX و فلت‌سام)

add_filter( 'posts_join', 'adel_search_join_sku', 10, 2 );
function adel_search_join_sku( $join, $query ) {
    global $wpdb;

    // توی ادمین معمولی کاری نکن، ولی اجازه بده توی AJAX اجرا بشه
    if ( is_admin() && ( ! function_exists('wp_doing_ajax') || ! wp_doing_ajax() ) ) {
        return $join;
    }

    // فقط روی کوئری‌هایی که برای product هستن
    $post_types = (array) $query->get( 'post_type' );
    if ( ! in_array( 'product', $post_types ) && ! empty( $post_types ) ) {
        return $join;
    }

    // فقط وقتی رشته جستجو وجود داره
    $search_term = $query->get( 's' );
    if ( empty( $search_term ) ) {
        return $join;
    }

    // جوین کردن متای _sku
    $join .= " LEFT JOIN {$wpdb->postmeta} AS sku_pm
               ON ({$wpdb->posts}.ID = sku_pm.post_id
               AND sku_pm.meta_key = '_sku') ";

    return $join;
}


add_filter( 'posts_where', 'adel_search_where_sku', 10, 2 );
function adel_search_where_sku( $where, $query ) {
    global $wpdb;

    // توی ادمین معمولی کاری نکن، ولی برای AJAX اجازه بده
    if ( is_admin() && ( ! function_exists('wp_doing_ajax') || ! wp_doing_ajax() ) ) {
        return $where;
    }

    $post_types = (array) $query->get( 'post_type' );
    if ( ! in_array( 'product', $post_types ) && ! empty( $post_types ) ) {
        return $where;
    }

    $search_term = $query->get( 's' );
    if ( empty( $search_term ) ) {
        return $where;
    }

    // سرچ جزئی روی SKU
    $like = '%' . $wpdb->esc_like( $search_term ) . '%';
    $where .= $wpdb->prepare( " OR (sku_pm.meta_value LIKE %s)", $like );

    return $where;
}


// جلوگیری از نتایج تکراری وقتی روی محصول و سرچ هستیم
add_filter( 'posts_distinct', 'adel_search_distinct_sku', 10, 2 );
function adel_search_distinct_sku( $distinct, $query ) {

    if ( is_admin() && ( ! function_exists('wp_doing_ajax') || ! wp_doing_ajax() ) ) {
        return $distinct;
    }

    $post_types   = (array) $query->get( 'post_type' );
    $search_term  = $query->get( 's' );

    if ( in_array( 'product', $post_types ) && ! empty( $search_term ) ) {
        return 'DISTINCT';
    }

    return $distinct;
}

if (!defined('ABSPATH')) exit;

/**
 * FD OFF - Full (Flatsome friendly)
 * - نمایش محصولات OFF داخل لیست محصولات همان دسته‌بندی سایت ۱ (بدون اینکه جدا مشخص شود)
 * - منوی تنظیمات: نمایش اول/آخر + متن و رنگ برچسب + انیمیشن دور کادر (رنگ + شدت)
 * - زیر هر کارت OFF فقط 3 خط:
 *   1) قیمت بازار (خط خورده)
 *   2) سود شما از خرید
 *   3) قیمت نهایی پرداخت شما
 * - هیچ تغییری روی سبد خرید/پرداخت سایت ۱ ندارد (فقط لینک به OFF)
 *
 * نکته: اگر CK/CS نگذاری هم ممکنه کار کند چون از Store API هم می‌تواند بخواند،
 * ولی برای رتبه‌بندی/فیلدهای اضافی، CK/CS بهتر است.
 */

/** ====== تنظیمات اتصال (اختیاری اما پیشنهاد می‌شود) ====== */
define('FD_OFF_BASE', 'https://faryazandecor.com/OFF');
define('FD_OFF_CK', 'ck_...'); // کلید Read
define('FD_OFF_CS', 'cs_...'); // سکرت Read
/** ========================================================= */

define('FD_OFF_CAT_CACHE_SEC', 3600);
define('FD_OFF_PROD_CACHE_SEC', 600);
define('FD_OFF_PER_PAGE_FALLBACK', 12);

/** option keys */
define('FD_OFF_OPT_GROUP', 'fd_off_opts');
define('FD_OFF_OPT_SHOW_FIRST',     'fd_off_show_first');      // 1/0
define('FD_OFF_OPT_LABEL_ENABLE',   'fd_off_label_enable');    // 1/0
define('FD_OFF_OPT_LABEL_TEXT',     'fd_off_label_text');      // string
define('FD_OFF_OPT_LABEL_COLOR',    'fd_off_label_color');     // hex
define('FD_OFF_OPT_ANIM_ENABLE',    'fd_off_anim_enable');     // 1/0
define('FD_OFF_OPT_ANIM_COLOR',     'fd_off_anim_color');      // hex
define('FD_OFF_OPT_ANIM_INTENSITY', 'fd_off_anim_intensity');  // 0..100

/* ========= Settings Page ========= */
add_action('admin_menu', function () {
    add_menu_page(
        'تنظیمات محصولات OFF',
        'محصولات OFF',
        'manage_woocommerce',
        'fd-off-settings',
        'fd_off_render_settings_page',
        'dashicons-megaphone',
        56
    );
});

add_action('admin_init', function () {
    register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_SHOW_FIRST, [
        'type' => 'integer',
        'sanitize_callback' => fn($v) => (int)(!!$v),
        'default' => 0,
    ]);

    register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_LABEL_ENABLE, [
        'type' => 'integer',
        'sanitize_callback' => fn($v) => (int)(!!$v),
        'default' => 1,
    ]);

    register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_LABEL_TEXT, [
        'type' => 'string',
        'sanitize_callback' => function ($v) {
            $v = wp_strip_all_tags((string)$v);
            return mb_substr($v, 0, 80);
        },
        'default' => 'قیمت ویژه اعضا',
    ]);

    register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_LABEL_COLOR, [
        'type' => 'string',
        'sanitize_callback' => function ($v) {
            $v = trim((string)$v);
            if (preg_match('/^#[0-9a-fA-F]{6}$/', $v)) return $v;
            return '#E53935';
        },
        'default' => '#E53935',
    ]);

    register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_ANIM_ENABLE, [
        'type' => 'integer',
        'sanitize_callback' => fn($v) => (int)(!!$v),
        'default' => 1,
    ]);

    register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_ANIM_COLOR, [
        'type' => 'string',
        'sanitize_callback' => function ($v) {
            $v = trim((string)$v);
            if (preg_match('/^#[0-9a-fA-F]{6}$/', $v)) return $v;
            return '#E53935';
        },
        'default' => '#E53935',
    ]);

    register_setting(FD_OFF_OPT_GROUP, FD_OFF_OPT_ANIM_INTENSITY, [
        'type' => 'integer',
        'sanitize_callback' => function ($v) {
            $v = (int)$v;
            if ($v < 0) $v = 0;
            if ($v > 100) $v = 100;
            return $v;
        },
        'default' => 75,
    ]);
});

function fd_off_render_settings_page() {
    if (!current_user_can('manage_woocommerce')) return;

    $show_first     = (int)get_option(FD_OFF_OPT_SHOW_FIRST, 0);

    $label_on       = (int)get_option(FD_OFF_OPT_LABEL_ENABLE, 1);
    $label_txt      = (string)get_option(FD_OFF_OPT_LABEL_TEXT, 'قیمت ویژه اعضا');
    $label_col      = (string)get_option(FD_OFF_OPT_LABEL_COLOR, '#E53935');

    $anim_on        = (int)get_option(FD_OFF_OPT_ANIM_ENABLE, 1);
    $anim_color     = (string)get_option(FD_OFF_OPT_ANIM_COLOR, '#E53935');
    $anim_intensity = (int)get_option(FD_OFF_OPT_ANIM_INTENSITY, 75);
    ?>
    <div class="wrap">
        <h1>تنظیمات محصولات OFF</h1>

        <form method="post" action="options.php">
            <?php settings_fields(FD_OFF_OPT_GROUP); ?>

            <table class="form-table" role="presentation">
                <tr>
                    <th scope="row">نمایش محصولات OFF اول لیست</th>
                    <td>
                        <label>
                            <input type="checkbox" name="<?php echo esc_attr(FD_OFF_OPT_SHOW_FIRST); ?>" value="1" <?php checked(1, $show_first); ?> />
                            اگر فعال شود، محصولات OFF قبل از محصولات سایت ۱ نمایش داده می‌شوند.
                        </label>
                    </td>
                </tr>

                <tr>
                    <th scope="row">برچسب روی محصولات OFF</th>
                    <td>
                        <label>
                            <input type="checkbox" name="<?php echo esc_attr(FD_OFF_OPT_LABEL_ENABLE); ?>" value="1" <?php checked(1, $label_on); ?> />
                            برچسب فعال باشد
                        </label>

                        <div style="margin-top:10px;">
                            <label>متن برچسب:</label><br>
                            <input type="text" class="regular-text"
                                   name="<?php echo esc_attr(FD_OFF_OPT_LABEL_TEXT); ?>"
                                   value="<?php echo esc_attr($label_txt); ?>" />
                        </div>

                        <div style="margin-top:10px;">
                            <label>رنگ پس‌زمینه برچسب:</label><br>
                            <input type="color"
                                   name="<?php echo esc_attr(FD_OFF_OPT_LABEL_COLOR); ?>"
                                   value="<?php echo esc_attr($label_col); ?>" />
                        </div>
                    </td>
                </tr>

                <tr>
                    <th scope="row">جلب توجه (انیمیشن دور کادر)</th>
                    <td>
                        <label>
                            <input type="checkbox" name="<?php echo esc_attr(FD_OFF_OPT_ANIM_ENABLE); ?>" value="1" <?php checked(1, $anim_on); ?> />
                            فعال باشد
                        </label>

                        <div style="margin-top:10px;">
                            <label>رنگ انیمیشن دور کادر:</label><br>
                            <input type="color"
                                   name="<?php echo esc_attr(FD_OFF_OPT_ANIM_COLOR); ?>"
                                   value="<?php echo esc_attr($anim_color); ?>" />
                        </div>

                        <div style="margin-top:10px; max-width:420px;">
                            <label>شدت انیمیشن (درصد): <strong><?php echo (int)$anim_intensity; ?>%</strong></label>
                            <input type="range"
                                   name="<?php echo esc_attr(FD_OFF_OPT_ANIM_INTENSITY); ?>"
                                   min="0" max="100" step="1"
                                   value="<?php echo (int)$anim_intensity; ?>"
                                   style="width:100%;" />
                        </div>
                    </td>
                </tr>
            </table>

            <?php submit_button('ذخیره تنظیمات'); ?>
        </form>
    </div>
    <?php
}

/* ========= Helpers ========= */
function fd_off_is_admin_view(): bool {
    return is_user_logged_in() && current_user_can('manage_woocommerce');
}

function fd_off_remote_get_json($url, $cache_sec = 600) {
    if (fd_off_is_admin_view()) $cache_sec = 0;

    $key = 'fd_off_json_' . md5($url);
    if ($cache_sec > 0) {
        $cached = get_transient($key);
        if ($cached !== false) return $cached;
    }

    $res = wp_remote_get($url, ['timeout' => 20, 'headers' => ['Accept' => 'application/json']]);
    if (is_wp_error($res)) return $res;

    $code = wp_remote_retrieve_response_code($res);
    $body = wp_remote_retrieve_body($res);
    if ($code < 200 || $code >= 300) {
        return new WP_Error('fd_off_http', 'HTTP ' . $code . ' - ' . wp_strip_all_tags($body));
    }

    $json = json_decode($body, true);
    if (!is_array($json)) return new WP_Error('fd_off_json', 'Invalid JSON');

    if ($cache_sec > 0) set_transient($key, $json, $cache_sec);
    return $json;
}

function fd_off_slugs_match($a, $b): bool {
    return strtolower(rawurldecode((string)$a)) === strtolower(rawurldecode((string)$b));
}

function fd_off_get_off_cat_id_by_slug($slug): int {
    $slug = (string)$slug;
    if ($slug === '') return 0;

    // فقط اگر CK/CS گذاشته شده باشد v3 را صدا می‌زنیم (وگرنه می‌افتد روی Store API)
    $has_keys = (strpos(FD_OFF_CK, 'ck_') === 0) && (strpos(FD_OFF_CS, 'cs_') === 0);

    if ($has_keys) {
        $url_v3 = rtrim(FD_OFF_BASE,'/') . '/wp-json/wc/v3/products/categories?per_page=100'
            . '&consumer_key=' . rawurlencode(FD_OFF_CK)
            . '&consumer_secret=' . rawurlencode(FD_OFF_CS);

        $cats = fd_off_remote_get_json($url_v3, FD_OFF_CAT_CACHE_SEC);
        if (!is_wp_error($cats) && is_array($cats)) {
            foreach ($cats as $c) {
                if (!empty($c['slug']) && fd_off_slugs_match($c['slug'], $slug)) return (int)($c['id'] ?? 0);
            }
        }
    }

    // Store API fallback
    $url_store = rtrim(FD_OFF_BASE,'/') . '/wp-json/wc/store/v1/products/categories?per_page=100';
    $cats2 = fd_off_remote_get_json($url_store, FD_OFF_CAT_CACHE_SEC);
    if (is_wp_error($cats2)) return 0;

    foreach ($cats2 as $c) {
        if (!empty($c['slug']) && fd_off_slugs_match($c['slug'], $slug)) return (int)($c['id'] ?? 0);
    }
    return 0;
}

function fd_off_get_products_for_off_cat($off_cat_id, $per_page, $page) {
    $has_keys = (strpos(FD_OFF_CK, 'ck_') === 0) && (strpos(FD_OFF_CS, 'cs_') === 0);

    if ($has_keys) {
        $url_v3 = rtrim(FD_OFF_BASE,'/') . '/wp-json/wc/v3/products?status=publish'
            . '&per_page=' . (int)$per_page
            . '&page=' . (int)$page
            . '&category=' . (int)$off_cat_id
            . '&consumer_key=' . rawurlencode(FD_OFF_CK)
            . '&consumer_secret=' . rawurlencode(FD_OFF_CS);

        $prods = fd_off_remote_get_json($url_v3, FD_OFF_PROD_CACHE_SEC);
        if (!is_wp_error($prods) && is_array($prods)) {
            // اگر rank_90d وجود داشت، بر اساسش مرتب می‌کنه
            usort($prods, fn($a,$b) => (int)($b['rank_90d'] ?? 0) <=> (int)($a['rank_90d'] ?? 0));
            return $prods;
        }
    }

    // Store API fallback
    $url_store = rtrim(FD_OFF_BASE,'/') . '/wp-json/wc/store/v1/products?per_page='.(int)$per_page
        . '&page='.(int)$page
        . '&category='.(int)$off_cat_id;

    return fd_off_remote_get_json($url_store, FD_OFF_PROD_CACHE_SEC);
}

function fd_off_best_image($p): string {
    if (!empty($p['images'][0]['src'])) return (string)$p['images'][0]['src']; // v3
    if (!empty($p['images'][0]['thumbnail'])) return (string)$p['images'][0]['thumbnail']; // store
    if (!empty($p['images'][0]['src'])) return (string)$p['images'][0]['src'];
    return '';
}

function fd_off_product_link($p): string {
    return !empty($p['permalink']) ? (string)$p['permalink'] : (!empty($p['url']) ? (string)$p['url'] : '');
}

function fd_off_num_from_any($v): float {
    $v = (string)$v;
    $v = preg_replace('/[^\d\.]/', '', $v);
    return $v === '' ? 0 : (float)$v;
}

function fd_off_format_toman($amount): string {
    $amount = (int) round($amount);
    if ($amount <= 0) return '';
    return number_format_i18n($amount) . ' تومان';
}

/**
 * فقط 3 خط زیر محصول OFF:
 * - قیمت بازار (فقط عدد خط خورده، متن واضح بماند)
 * - سود شما از خرید
 * - قیمت نهایی پرداخت شما
 */
function fd_off_price_html($p): string {

    $regular = 0; $sale = 0; $current = 0;

    // v3
    if (isset($p['regular_price']) || isset($p['sale_price']) || isset($p['price'])) {
        $regular = fd_off_num_from_any($p['regular_price'] ?? '');
        $sale    = fd_off_num_from_any($p['sale_price'] ?? '');
        $current = fd_off_num_from_any($p['price'] ?? '');
    }

    // store fallback
    if (($regular <= 0 && $sale <= 0 && $current <= 0) && !empty($p['prices'])) {
        $regular = fd_off_num_from_any($p['prices']['regular_price'] ?? '');
        $sale    = fd_off_num_from_any($p['prices']['sale_price'] ?? '');
        $current = fd_off_num_from_any($p['prices']['price'] ?? '');
    }

    // قیمت نهایی پرداخت شما
    $final = ($sale > 0) ? $sale : $current;

    // fallback price_html (اگر رنج قیمت بود یا نشد حساب کرد)
    $fallback = '';
    if (!empty($p['price_html'])) $fallback = (string)$p['price_html'];
    if (!empty($p['prices']['price_html'])) $fallback = (string)$p['prices']['price_html'];

    if ($regular <= 0 || $final <= 0 || $regular <= $final) {
        // اگر نتونستیم سود رو درست حساب کنیم، فقط قیمت نهایی رو نشون بده
        if ($final > 0) {
            return '<div class="fd-off-pricebox"><div class="fd-off-lines">'
                . '<div class="fd-off-line">قیمت نهایی پرداخت شما: <b class="fd-off-final">' . esc_html(fd_off_format_toman($final)) . '</b></div>'
                . '</div></div>';
        }
        if ($fallback !== '') {
            return '<div class="fd-off-pricebox"><div class="fd-off-lines">' . wp_kses_post($fallback) . '</div></div>';
        }
        return '';
    }

    $profit = $regular - $final;

    $out  = '<div class="fd-off-pricebox">';
    $out .= '<div class="fd-off-lines">';
    $out .= '<div class="fd-off-line fd-off-market"><span class="fd-off-k">قیمت بازار:</span> <del class="fd-off-mkt">' . esc_html(fd_off_format_toman($regular)) . '</del></div>';
    $out .= '<div class="fd-off-line fd-off-profit"><span class="fd-off-k">سود شما از خرید:</span> <b>' . esc_html(fd_off_format_toman($profit)) . '</b></div>';
    $out .= '<div class="fd-off-line fd-off-pay"><span class="fd-off-k">قیمت نهایی پرداخت شما:</span> <b class="fd-off-final">' . esc_html(fd_off_format_toman($final)) . '</b></div>';
    $out .= '</div></div>';

    return $out;
}

/* ========= Render items ========= */
function fd_off_render_items($products): string {
    if (!is_array($products) || empty($products)) return '';

    $label_on  = (int)get_option(FD_OFF_OPT_LABEL_ENABLE, 1) === 1;
    $label_txt = (string)get_option(FD_OFF_OPT_LABEL_TEXT, 'قیمت ویژه اعضا');
    $label_col = (string)get_option(FD_OFF_OPT_LABEL_COLOR, '#E53935');
    $anim_on   = (int)get_option(FD_OFF_OPT_ANIM_ENABLE, 1) === 1;

    ob_start();
    foreach ($products as $p) {
        $name  = $p['name'] ?? '';
        $link  = fd_off_product_link($p);
        $img   = fd_off_best_image($p);
        $price = fd_off_price_html($p);

        $anim_class = $anim_on ? ' fd-off-anim' : '';

        echo '<div class="product-small col has-hover fd-off-product type-product product-type-external' . esc_attr($anim_class) . '">';
          echo '<div class="col-inner">';

            if ($label_on && $label_txt !== '') {
                // برچسب بالاتر از عکس (روی خود عکس نیوفته)
                echo '<span class="fd-off-badge" style="background:' . esc_attr($label_col) . ';">' . esc_html($label_txt) . '</span>';
            }

            echo '<div class="product-small box">';

              echo '<div class="box-image"><div class="image-fade_in_back">';
                echo '<a href="' . esc_url($link) . '" target="_blank" rel="nofollow sponsored noopener">';
                  if ($img) {
                      echo '<img class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail wp-post-image" src="' . esc_url($img) . '" alt="' . esc_attr($name) . '" loading="lazy" />';
                  }
                echo '</a>';
              echo '</div></div>';

              echo '<div class="box-text box-text-products text-center grid-style-2">';
                echo '<p class="name product-title"><a href="' . esc_url($link) . '" target="_blank" rel="nofollow sponsored noopener">' . esc_html($name) . '</a></p>';
                echo $price;
                echo '<a class="button" href="' . esc_url($link) . '" target="_blank" rel="nofollow sponsored noopener">مشاهده</a>';
              echo '</div>';

            echo '</div>';
          echo '</div>';
        echo '</div>';
    }
    return ob_get_clean();
}

/* ========= Build for current category ========= */
function fd_off_build_html_for_current_cat() {
    if (!class_exists('WooCommerce')) return '';
    if (!is_product_category()) return '';

    $term = get_queried_object();
    if (!$term || empty($term->slug)) return '';

    $off_cat_id = fd_off_get_off_cat_id_by_slug($term->slug);
    if (!$off_cat_id) return '';

    $per_page = function_exists('wc_get_loop_prop') ? (int) wc_get_loop_prop('per_page') : 0;
    if ($per_page <= 0) $per_page = FD_OFF_PER_PAGE_FALLBACK;

    $paged = max(1, (int)get_query_var('paged'));

    $products = fd_off_get_products_for_off_cat($off_cat_id, $per_page, $paged);

    if (is_wp_error($products)) {
        if (fd_off_is_admin_view()) {
            return '<div class="woocommerce-error" style="margin:10px 0;">OFF API Error: '
                . esc_html($products->get_error_message()) . '</div>';
        }
        return '';
    }

    return fd_off_render_items($products);
}

/* ========= Inject: FIRST or LAST ========= */
add_filter('woocommerce_product_loop_start', function ($start) {
    if ((int)get_option(FD_OFF_OPT_SHOW_FIRST, 0) !== 1) return $start;

    $off_html = fd_off_build_html_for_current_cat();
    if (!$off_html) return $start;
    return $start . $off_html;
}, 20);

add_filter('woocommerce_product_loop_end', function ($end) {
    if ((int)get_option(FD_OFF_OPT_SHOW_FIRST, 0) === 1) return $end;

    $off_html = fd_off_build_html_for_current_cat();
    if (!$off_html) return $end;
    return $off_html . $end;
}, 20);

/* ========= CSS ========= */
add_action('wp_head', function () {

    $hex = (string)get_option(FD_OFF_OPT_ANIM_COLOR, '#E53935');
    $intensity = (int)get_option(FD_OFF_OPT_ANIM_INTENSITY, 75);
    if ($intensity < 0) $intensity = 0;
    if ($intensity > 100) $intensity = 100;
    if (!preg_match('/^#[0-9a-fA-F]{6}$/', $hex)) $hex = '#E53935';

    $r = hexdec(substr($hex, 1, 2));
    $g = hexdec(substr($hex, 3, 2));
    $b = hexdec(substr($hex, 5, 2));

    $alphaBorder = 0.10 + (0.75 * ($intensity / 100)); // 0.10..0.85
    $alphaShadow = 0.06 + (0.45 * ($intensity / 100)); // 0.06..0.51
    $spreadPx    = 6 + (16 * ($intensity / 100));      // 6..22
    ?>
<style>
  .fd-off-product .col-inner{
    position:relative !important;
    border-radius:12px;
  }

  /* تصویر */
  .fd-off-product .box-image{
    height:280px !important;
    display:flex !important;
    align-items:center !important;
    justify-content:center !important;
    overflow:hidden !important;
    position:relative !important;
    border-radius:12px;
  }
  .fd-off-product .box-image img{
    width:100% !important;
    height:100% !important;
    object-fit:contain !important;
    display:block !important;
  }

  /* برچسب */
  .fd-off-badge{
    position:absolute !important;
    top:-14px !important;
    right:12px !important;
    z-index:9999 !important;
    padding:9px 14px !important;
    border-radius:999px !important;
    font-size:13px !important;
    line-height:1 !important;
    color:#fff !important;
    font-weight:900 !important;
    box-shadow:0 10px 22px rgba(0,0,0,.18) !important;
    white-space:nowrap !important;
    max-width:calc(100% - 24px);
    overflow:hidden;
    text-overflow:ellipsis;
  }

  /* تایپوگرافی نزدیک به Flatsome */
  .fd-off-product .box-text{
    padding-top:8px !important;
  }
  .fd-off-product .product-title a{
    font-size:16px !important;
    font-weight:800 !important;
    line-height:1.6 !important;
  }

  /* باکس قیمت‌ها - خوانا */
  .fd-off-pricebox{
    margin:10px 0 12px;
    text-align:right;
    background:rgba(0,0,0,.03);
    border:1px solid rgba(0,0,0,.06);
    border-radius:10px;
    padding:10px 10px 8px;
  }
  .fd-off-lines{
    font-size:13px;
    line-height:2.05;
    color:#111;
  }
  .fd-off-k{
    font-weight:800;
    color:#222;
  }
  .fd-off-mkt{
    color:#444;
    font-weight:800;
    text-decoration-thickness:0.5px;
  }
  .fd-off-profit{
    color:#b30000;
    font-weight:900;
  }
  .fd-off-final{
    font-weight:900;
  }

  /* انیمیشن دور کادر */
  .fd-off-anim .col-inner:before{
    content:"";
    position:absolute;
    inset:-2px;
    border-radius:14px;
    pointer-events:none;
    border:1px solid rgba(<?php echo (int)$r;?>,<?php echo (int)$g;?>,<?php echo (int)$b;?>,<?php echo (float)$alphaBorder;?>);
    animation:fdOffPulse 1.25s ease-in-out infinite;
  }
  @keyframes fdOffPulse{
    0%   { transform:scale(1);   opacity:.35; box-shadow:0 0 0 0 rgba(<?php echo (int)$r;?>,<?php echo (int)$g;?>,<?php echo (int)$b;?>,0); }
    50%  { transform:scale(1.012);opacity:1;  box-shadow:0 0 0 <?php echo (int)$spreadPx; ?>px rgba(<?php echo (int)$r;?>,<?php echo (int)$g;?>,<?php echo (int)$b;?>,<?php echo (float)$alphaShadow;?>); }
    100% { transform:scale(1);   opacity:.35; box-shadow:0 0 0 0 rgba(<?php echo (int)$r;?>,<?php echo (int)$g;?>,<?php echo (int)$b;?>,0); }
  }

  @media (max-width: 900px){
    .fd-off-product .box-image{ height:240px !important; }
    .fd-off-badge{ top:-12px !important; right:10px !important; font-size:12px !important; }
    .fd-off-product .product-title a{ font-size:15px !important; }
  }
  @media (max-width: 520px){
    .fd-off-product .box-image{ height:210px !important; }
    .fd-off-badge{ top:-10px !important; right:8px !important; font-size:12px !important; }
    .fd-off-lines{ font-size:13px; line-height:2.05; }
  }
</style>
<?php
});


/* === Front Dot Indicator on Archives (Shop/Category) === */

if (!defined('ABSPATH')) exit;

function pcatc_dot_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01';
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_dot_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_dot_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_dot_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_dot_is_stale($id) {
	$ts = pcatc_dot_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_dot_cutoff_ts();
}

function pcatc_dot_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_dot_is_stale((int)$vid)) return true;
			}
		}
		return false;
	}

	// Simple / others: خود محصول
	return !pcatc_dot_is_stale((int)$product->get_id());
}

/**
 * Add dot next to price on archives (shop/category/tag)
 */
function pcatc_dot_price_html($price_html, $product) {
	if (is_admin()) return $price_html;

	// فقط صفحات لیست محصولات در سایت
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return $price_html;
	}

	// اگر قیمت خالیه، چیزی نزن
	if (trim(wp_strip_all_tags($price_html)) === '') return $price_html;

	$is_fresh = pcatc_dot_product_is_fresh($product);
	$dot = $is_fresh
		? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>'
		: '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>';

	// نقطه + فاصله + قیمت
	return $dot . ' ' . $price_html;
}
add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2);

/** CSS for dots (front) */
function pcatc_dot_css() {
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return;

	echo '<style>
	.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);}
	.pcatc-dot-green{background:#19a64a;}
	.pcatc-dot-red{background:#d10000;}
	</style>';
}
add_action('wp_head', 'pcatc_dot_css', 50);


/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */

if (!defined('ABSPATH')) exit;

class PCATC_Settings_Snippet {
	// Options
	const OPT_CUTOFF         = 'pcatc_cutoff_date';
	const OPT_MSG            = 'pcatc_message';
	const OPT_FALLBACK       = 'pcatc_use_modified_fallback';

	const OPT_SHOW_FRONT     = 'pcatc_show_front_status';
	const OPT_SHOW_ADMIN     = 'pcatc_show_admin_status';
	const OPT_TEXT_FRESH     = 'pcatc_text_fresh';
	const OPT_TEXT_STALE     = 'pcatc_text_stale';

	// Meta
	const META               = '_pcatc_price_last_updated';

	public function __construct() {
		// Admin settings UI
		add_action('admin_menu', [$this, 'add_settings_page']);
		add_action('admin_init', [$this, 'register_settings']);
		add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100);

		// Stamp when price changes
		add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4);
		add_action('added_post_meta',   [$this,'maybe_stamp_price_update'], 10, 4);

		// Block add to cart + notices
		add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5);
		add_action('woocommerce_before_cart',            [$this,'cart_checkout_notice']);
		add_action('woocommerce_before_checkout_form',   [$this,'cart_checkout_notice']);

		// Front indicators
		add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11);
		add_filter('woocommerce_available_variation',    [$this,'add_variation_status_data'], 10, 3);
		add_action('wp_enqueue_scripts',                 [$this,'enqueue_front_js']);

		// Admin list indicator
		add_filter('manage_edit-product_columns',        [$this,'add_admin_column'], 30);
		add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2);
		add_action('admin_head',                         [$this,'admin_column_css']);
	}

	/* ---------- Defaults ---------- */
	private function default_cutoff(): string { return '2026-01-01'; }
	private function default_msg(): string {
		return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.';
	}
	private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; }
	private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; }

	/* ---------- Options getters ---------- */
	private function get_cutoff_date(): string {
		$val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff());
		if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff();
		return $val;
	}
	private function get_msg(): string {
		$val = (string) get_option(self::OPT_MSG, $this->default_msg());
		return $val !== '' ? $val : $this->default_msg();
	}
	private function use_fallback(): bool {
		return get_option(self::OPT_FALLBACK, 'yes') === 'yes';
	}
	private function show_front(): bool {
		return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes';
	}
	private function show_admin(): bool {
		return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes';
	}
	private function text_fresh(): string {
		$val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh());
		return $val !== '' ? $val : $this->default_text_fresh();
	}
	private function text_stale(): string {
		$val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale());
		return $val !== '' ? $val : $this->default_text_stale();
	}

	/* ---------- Cutoff timestamp ---------- */
	private function cutoff_ts(): int {
		$dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone());
		return $dt->getTimestamp();
	}

	/* ---------- Price update stamp ---------- */
	public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void {
		$type = get_post_type($post_id);
		if (!in_array($type, ['product','product_variation'], true)) return;

		if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return;

		update_post_meta($post_id, self::META, time());
	}

	private function last_update_ts($id): int {
		$ts = (int) get_post_meta($id, self::META, true);
		if ($ts > 0) return $ts;

		if ($this->use_fallback()) {
			$post = get_post($id);
			if ($post && !empty($post->post_modified_gmt)) {
				$t = strtotime($post->post_modified_gmt . ' GMT');
				if ($t) return $t;
			}
		}
		return 0;
	}

	private function is_stale($id): bool {
		$ts = $this->last_update_ts($id);
		if ($ts <= 0) return true;
		return $ts < $this->cutoff_ts();
	}

	private function status_payload_for($id): array {
		$stale = $this->is_stale($id);
		return [
			'is_stale' => $stale ? 1 : 0,
			'text'     => $stale ? $this->text_stale() : $this->text_fresh(),
			'class'    => $stale ? 'pcatc-stale' : 'pcatc-fresh',
		];
	}

	/* ---------- WooCommerce blocking ---------- */
	public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) {
		$target_id = $variation_id ? (int)$variation_id : (int)$product_id;

		if ($this->is_stale($target_id)) {
			wc_add_notice($this->get_msg(), 'error');
			return false;
		}
		return $passed;
	}

	public function cart_checkout_notice(): void {
		if (!function_exists('WC') || !WC()->cart) return;

		foreach (WC()->cart->get_cart() as $item) {
			$target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id'];
			if ($this->is_stale($target_id)) {
				wc_print_notice($this->get_msg(), 'error');
				break;
			}
		}
	}

	/* ---------- Front status (simple + variable dynamic) ---------- */
	public function render_front_status_block(): void {
		if (!$this->show_front() || !is_product()) return;

		global $product;
		if (!$product instanceof WC_Product) return;

		// For simple products, render fixed status.
		// For variable products, we render a container that JS will update on variation selection.
		$is_variable = $product->is_type('variable');

		$payload = $this->status_payload_for($product->get_id());
		$text = esc_html($payload['text']);
		$cls  = esc_attr($payload['class']);

		echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">';
		echo $is_variable ? '' : $text;
		echo '</div>';
	}

	public function add_variation_status_data($variation_data, $product, $variation) {
		if (!$this->show_front()) return $variation_data;

		$vid = $variation->get_id();
		$p = $this->status_payload_for($vid);

		$variation_data['pcatc_is_stale'] = $p['is_stale'];
		$variation_data['pcatc_text']     = $p['text'];
		$variation_data['pcatc_class']    = $p['class'];

		return $variation_data;
	}

	public function enqueue_front_js(): void {
		if (!$this->show_front() || !is_product()) return;

		wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true);
		wp_enqueue_script('pcatc-front');

		// Inline CSS (front)
		$css = "
#pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;}
#pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;}
";
		wp_add_inline_style('woocommerce-inline', $css);

		// JS: update status when variation changes
		$js = <<<JS
jQuery(function($){
  var box = $('#pcatc-price-status');
  if(!box.length) return;

  var form = $('form.variations_form');
  if(!form.length) return; // simple product -> no need

  function setStatus(v){
    if(!v || typeof v.pcatc_is_stale === 'undefined'){
      // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده)
      box.text('');
      box.removeClass('pcatc-fresh pcatc-stale');
      return;
    }
    box.text(v.pcatc_text || '');
    box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || '');
  }

  form.on('found_variation', function(e, variation){
    setStatus(variation);
  });

  form.on('reset_data', function(){
    setStatus(null);
  });
});
JS;
		wp_add_inline_script('pcatc-front', $js);
	}

	/* ---------- Admin list column (green/red dot) ---------- */
	public function add_admin_column($columns) {
		if (!$this->show_admin()) return $columns;

		// Insert near price column if possible
		$new = [];
		foreach ($columns as $key => $label) {
			$new[$key] = $label;
			if ($key === 'price') {
				$new['pcatc_status'] = 'وضعیت قیمت';
			}
		}
		if (!isset($new['pcatc_status'])) {
			$new['pcatc_status'] = 'وضعیت قیمت';
		}
		return $new;
	}

	public function render_admin_column($column, $post_id) {
		if (!$this->show_admin()) return;
		if ($column !== 'pcatc_status') return;

		// For variable product: if ANY variation is fresh => green else red
		$product = wc_get_product($post_id);
		if (!$product) return;

		$is_fresh = false;

		if ($product->is_type('variable')) {
			$children = $product->get_children();
			if (!empty($children)) {
				foreach ($children as $vid) {
					if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; }
				}
			}
		} else {
			$is_fresh = !$this->is_stale($post_id);
		}

		echo $is_fresh
			? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>'
			: '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>';
	}

	public function admin_column_css() {
		if (!$this->show_admin()) return;
		echo '<style>
			.column-pcatc_status{width:80px;text-align:center;}
			.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;}
			.pcatc-dot-green{background:#19a64a;}
			.pcatc-dot-red{background:#d10000;}
		</style>';
	}

	/* ---------- Admin settings page ---------- */
	public function add_settings_page(): void {
		add_options_page(
			'تنظیمات قفل خرید بر اساس تاریخ',
			'قفل خرید (تاریخ قیمت)',
			'manage_options',
			'pcatc-settings',
			[$this, 'render_settings_page']
		);
	}

	public function register_settings(): void {
		register_setting('pcatc_settings_group', self::OPT_CUTOFF, [
			'type' => 'string',
			'sanitize_callback' => function($v){
				$v = trim((string)$v);
				return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff();
			}
		]);

		register_setting('pcatc_settings_group', self::OPT_MSG, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_textarea_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_FALLBACK, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);
	}

	public function render_settings_page(): void {
		if (!current_user_can('manage_options')) return;

		$cutoff = esc_attr($this->get_cutoff_date());
		$msg    = esc_textarea($this->get_msg());
		$fb     = $this->use_fallback() ? 'yes' : 'no';

		$sf     = $this->show_front() ? 'yes' : 'no';
		$sa     = $this->show_admin() ? 'yes' : 'no';

		$tf     = esc_attr($this->text_fresh());
		$ts     = esc_attr($this->text_stale());
		?>
		<div class="wrap">
			<h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1>
			<form method="post" action="options.php">
				<?php settings_fields('pcatc_settings_group'); ?>

				<table class="form-table" role="presentation">
					<tr>
						<th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th>
						<td>
							<input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>">
							<p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th>
						<td>
							<textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea>
							<p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th>
						<td>
							<label>
								<input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>>
								اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن.
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row">نمایش وضعیت قیمت</th>
						<td>
							<label style="display:block;margin-bottom:6px;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>>
								نمایش نقطه سبز/قرمز در لیست محصولات (پنل)
							</label>

							<label style="display:block;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>>
								نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای)
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;">
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;">
						</td>
					</tr>
				</table>

				<?php submit_button('ذخیره تنظیمات'); ?>
			</form>
		</div>
		<?php
	}

	/* ---------- Admin bar shortcut ---------- */
	public function admin_bar_link($admin_bar): void {
		if (!is_admin_bar_showing() || !current_user_can('manage_options')) return;
		$admin_bar->add_node([
			'id'    => 'pcatc_settings_link',
			'title' => 'تنظیمات قفل خرید',
			'href'  => admin_url('options-general.php?page=pcatc-settings'),
		]);
	}
}

new PCATC_Settings_Snippet();


if (!defined('ABSPATH')) exit;

if (!class_exists('KCN_Keep_Like_Code_App_V2')) {

    class KCN_Keep_Like_Code_App_V2 {

        private $option_key = 'kcn_stable_notes_data_v2';

        public function __construct() {
            add_shortcode('kcn_code_app', array($this, 'render_app'));

            add_action('wp_ajax_kcn_delete_note', array($this, 'ajax_delete_note'));
            add_action('wp_ajax_nopriv_kcn_delete_note', array($this, 'ajax_delete_note'));

            add_action('wp_ajax_kcn_toggle_fav', array($this, 'ajax_toggle_fav'));
            add_action('wp_ajax_nopriv_kcn_toggle_fav', array($this, 'ajax_toggle_fav'));

            add_action('wp_ajax_kcn_update_title', array($this, 'ajax_update_title'));
            add_action('wp_ajax_nopriv_kcn_update_title', array($this, 'ajax_update_title'));

            add_action('wp_ajax_kcn_bulk_delete', array($this, 'ajax_bulk_delete'));
            add_action('wp_ajax_nopriv_kcn_bulk_delete', array($this, 'ajax_bulk_delete'));
        }

        private function get_notes() {
            $notes = get_option($this->option_key, array());
            return is_array($notes) ? $notes : array();
        }

        private function save_notes($notes) {
            update_option($this->option_key, array_values($notes), false);
        }

        private function sanitize_code($code) {
            return wp_unslash($code);
        }

        private function handle_submit() {
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                return;
            }

            if (!isset($_POST['kcn_action']) || $_POST['kcn_action'] !== 'save_note') {
                return;
            }

            if (!isset($_POST['kcn_nonce']) || !wp_verify_nonce($_POST['kcn_nonce'], 'kcn_save_note')) {
                return;
            }

            $title = isset($_POST['kcn_title']) ? sanitize_text_field(wp_unslash($_POST['kcn_title'])) : '';
            $language = isset($_POST['kcn_language']) ? sanitize_text_field(wp_unslash($_POST['kcn_language'])) : 'text';
            $code = isset($_POST['kcn_code']) ? $this->sanitize_code($_POST['kcn_code']) : '';

            if ($title === '' || trim($code) === '') {
                return;
            }

            $notes = $this->get_notes();

            $notes[] = array(
                'id' => uniqid('kcn_', true),
                'title' => $title,
                'language' => $language,
                'code' => $code,
                'created' => current_time('mysql'),
                'fav' => 0,
            );

            $this->save_notes($notes);
        }

        private function find_note_index($note_id, $notes) {
            foreach ($notes as $i => $note) {
                if (isset($note['id']) && $note['id'] === $note_id) {
                    return $i;
                }
            }
            return -1;
        }

        public function ajax_delete_note() {
            check_ajax_referer('kcn_ajax_nonce', 'nonce');

            $note_id = isset($_POST['note_id']) ? sanitize_text_field(wp_unslash($_POST['note_id'])) : '';
            if (!$note_id) {
                wp_send_json_error(array('message' => 'شناسه نامعتبر است'));
            }

            $notes = $this->get_notes();
            $new_notes = array();

            foreach ($notes as $note) {
                if (!isset($note['id']) || $note['id'] !== $note_id) {
                    $new_notes[] = $note;
                }
            }

            $this->save_notes($new_notes);
            wp_send_json_success(array('note_id' => $note_id));
        }

        public function ajax_toggle_fav() {
            check_ajax_referer('kcn_ajax_nonce', 'nonce');

            $note_id = isset($_POST['note_id']) ? sanitize_text_field(wp_unslash($_POST['note_id'])) : '';
            if (!$note_id) {
                wp_send_json_error(array('message' => 'شناسه نامعتبر است'));
            }

            $notes = $this->get_notes();
            $index = $this->find_note_index($note_id, $notes);

            if ($index < 0) {
                wp_send_json_error(array('message' => 'یادداشت پیدا نشد'));
            }

            $notes[$index]['fav'] = empty($notes[$index]['fav']) ? 1 : 0;
            $fav = $notes[$index]['fav'];

            $this->save_notes($notes);
            wp_send_json_success(array('fav' => $fav));
        }

        public function ajax_update_title() {
            check_ajax_referer('kcn_ajax_nonce', 'nonce');

            $note_id = isset($_POST['note_id']) ? sanitize_text_field(wp_unslash($_POST['note_id'])) : '';
            $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';

            if (!$note_id || $title === '') {
                wp_send_json_error(array('message' => 'اطلاعات ناقص است'));
            }

            $notes = $this->get_notes();
            $index = $this->find_note_index($note_id, $notes);

            if ($index < 0) {
                wp_send_json_error(array('message' => 'یادداشت پیدا نشد'));
            }

            $notes[$index]['title'] = $title;
            $this->save_notes($notes);

            wp_send_json_success(array('title' => $title));
        }

        public function ajax_bulk_delete() {
            check_ajax_referer('kcn_ajax_nonce', 'nonce');

            $ids = isset($_POST['ids']) ? (array) $_POST['ids'] : array();
            $ids = array_map('sanitize_text_field', $ids);
            $ids = array_filter($ids);

            if (empty($ids)) {
                wp_send_json_error(array('message' => 'موردی انتخاب نشده'));
            }

            $notes = $this->get_notes();
            $new_notes = array();

            foreach ($notes as $note) {
                if (!isset($note['id']) || !in_array($note['id'], $ids, true)) {
                    $new_notes[] = $note;
                }
            }

            $this->save_notes($new_notes);
            wp_send_json_success(array('deleted_ids' => $ids));
        }

        private function render_styles() {
            ?>
            <style>
                .kcn-wrap{
                    max-width:1000px;
                    margin:20px auto;
                    padding:14px;
                    font-family:Tahoma, Arial, sans-serif;
                    direction:rtl;
                }
                .kcn-card{
                    background:#fff;
                    border:1px solid #e5e7eb;
                    border-radius:16px;
                    padding:14px;
                    margin-bottom:16px;
                    box-shadow:0 4px 14px rgba(0,0,0,.05);
                }
                .kcn-title{
                    margin:0 0 14px;
                    font-size:19px;
                    font-weight:700;
                    color:#111827;
                }
                .kcn-field{margin-bottom:14px;}
                .kcn-label{
                    display:block;
                    margin-bottom:8px;
                    font-weight:600;
                    color:#111827;
                    font-size:14px;
                }
                .kcn-input,.kcn-select,.kcn-textarea{
                    width:100%;
                    box-sizing:border-box;
                    border:1px solid #d1d5db;
                    border-radius:12px;
                    background:#fff;
                    color:#111827;
                    padding:12px 14px;
                    font-size:14px;
                    outline:none;
                }
                .kcn-textarea{
                    min-height:220px;
                    resize:vertical;
                    font-family:Consolas, Monaco, monospace;
                    line-height:1.7;
                    direction:ltr;
                    text-align:left;
                    white-space:pre;
                    unicode-bidi:plaintext;
                }
                .kcn-btn{
                    display:inline-block;
                    border:none;
                    border-radius:12px;
                    padding:9px 14px;
                    cursor:pointer;
                    font-size:13px;
                    font-weight:700;
                    text-decoration:none;
                }
                .kcn-btn-primary{background:#2563eb;color:#fff;}
                .kcn-btn-light{background:#f3f4f6;color:#111827;}
                .kcn-btn-danger{background:#dc2626;color:#fff;}
                .kcn-btn-star{
                    background:#fff7ed;
                    color:#9a3412;
                    border:1px solid #fdba74;
                }
                .kcn-toolbar{
                    display:flex;
                    gap:8px;
                    flex-wrap:wrap;
                    margin-bottom:14px;
                }
                .kcn-list{
                    display:grid;
                    gap:12px;
                }
                .kcn-note{
                    background:#fff;
                    border:1px solid #e5e7eb;
                    border-radius:16px;
                    overflow:hidden;
                    position:relative;
                }
                .kcn-note.fav{
                    border-color:#f59e0b;
                    box-shadow:0 0 0 2px rgba(245,158,11,.12);
                }
                .kcn-note-head{
                    padding:14px;
                }
                .kcn-row-top{
                    display:flex;
                    align-items:flex-start;
                    gap:10px;
                }
                .kcn-check{
                    margin-top:3px;
                    flex:0 0 auto;
                }
                .kcn-main{
                    flex:1 1 auto;
                    min-width:0;
                }
                .kcn-note-name{
                    font-weight:700;
                    color:#111827;
                    margin-bottom:6px;
                    font-size:15px;
                    word-break:break-word;
                }
                .kcn-note-name-input{
                    width:100%;
                    border:1px solid #d1d5db;
                    border-radius:10px;
                    padding:8px 10px;
                    font-size:14px;
                    color:#111827;
                    background:#fff;
                    box-sizing:border-box;
                }
                .kcn-note-sub{
                    font-size:12px;
                    color:#6b7280;
                    margin-bottom:10px;
                }
                .kcn-preview{
                    font-family:Consolas, Monaco, monospace;
                    font-size:12px;
                    line-height:1.7;
                    color:#374151;
                    background:#f9fafb;
                    border:1px solid #eef2f7;
                    border-radius:12px;
                    padding:12px;
                    direction:ltr;
                    text-align:left;
                    white-space:pre-wrap;
                    word-break:break-word;
                    display:-webkit-box;
                    -webkit-line-clamp:3;
                    -webkit-box-orient:vertical;
                    overflow:hidden;
                }
                .kcn-full{
                    display:none;
                    margin-top:10px;
                }
                .kcn-full.open{
                    display:block;
                }
                .kcn-code{
                    margin:0;
                    padding:14px;
                    background:#ffffff;
                    border:1px solid #e5e7eb;
                    border-radius:12px;
                    color:#111827;
                    font-size:12px;
                    line-height:1.75;
                    direction:ltr;
                    text-align:left;
                    white-space:pre-wrap;
                    word-break:break-word;
                    overflow:auto;
                    font-family:Consolas, Monaco, monospace;
                    max-height:420px;
                }
                .kcn-actions{
                    display:flex;
                    gap:8px;
                    flex-wrap:wrap;
                    margin-top:12px;
                }
                .kcn-empty{
                    color:#6b7280;
                    font-size:14px;
                }
                .kcn-save-title-wrap{
                    display:none;
                    gap:8px;
                    margin-bottom:10px;
                }
                .kcn-save-title-wrap.open{
                    display:flex;
                }
                .kcn-status{
                    font-size:12px;
                    color:#16a34a;
                    margin-top:6px;
                    display:none;
                }
                .kcn-status.show{
                    display:block;
                }
                @media (max-width:768px){
                    .kcn-wrap{padding:10px;}
                    .kcn-card{padding:12px;}
                    .kcn-btn{flex:1 1 100%;text-align:center;}
                    .kcn-code{max-height:300px;}
                    .kcn-row-top{align-items:flex-start;}
                }
            </style>
            <?php
        }

        private function render_script() {
            $ajax_url = admin_url('admin-ajax.php');
            $nonce = wp_create_nonce('kcn_ajax_nonce');
            ?>
            <script>
                document.addEventListener('DOMContentLoaded', function(){
                    var wrap = document.querySelector('.kcn-wrap');
                    if (!wrap) return;

                    function postAjax(action, data) {
                        var fd = new FormData();
                        fd.append('action', action);
                        fd.append('nonce', '<?php echo esc_js($nonce); ?>');

                        Object.keys(data).forEach(function(key){
                            if (Array.isArray(data[key])) {
                                data[key].forEach(function(v){
                                    fd.append(key + '[]', v);
                                });
                            } else {
                                fd.append(key, data[key]);
                            }
                        });

                        return fetch('<?php echo esc_url($ajax_url); ?>', {
                            method: 'POST',
                            body: fd,
                            credentials: 'same-origin'
                        }).then(function(r){ return r.json(); });
                    }

                    document.addEventListener('click', function(e){

                        var toggleBtn = e.target.closest('.kcn-toggle-btn');
                        if (toggleBtn) {
                            e.preventDefault();
                            var targetId = toggleBtn.getAttribute('data-target');
                            var box = document.getElementById(targetId);
                            if (!box) return;

                            if (box.classList.contains('open')) {
                                box.classList.remove('open');
                                toggleBtn.textContent = 'نمایش بیشتر';
                            } else {
                                box.classList.add('open');
                                toggleBtn.textContent = 'بستن';
                            }
                            return;
                        }

                        var copyBtn = e.target.closest('.kcn-copy-btn');
                        if (copyBtn) {
                            e.preventDefault();
                            var targetId = copyBtn.getAttribute('data-target');
                            var codeEl = document.getElementById(targetId);
                            if (!codeEl) return;

                            var text = codeEl.innerText || codeEl.textContent || '';
                            var oldText = copyBtn.textContent;

                            function done() {
                                copyBtn.textContent = 'کپی شد';
                                setTimeout(function(){ copyBtn.textContent = oldText; }, 1500);
                            }

                            if (navigator.clipboard && window.isSecureContext) {
                                navigator.clipboard.writeText(text).then(done).catch(function(){
                                    fallbackCopy(text, done);
                                });
                            } else {
                                fallbackCopy(text, done);
                            }

                            function fallbackCopy(text, callback) {
                                var ta = document.createElement('textarea');
                                ta.value = text;
                                ta.style.position = 'fixed';
                                ta.style.left = '-9999px';
                                document.body.appendChild(ta);
                                ta.focus();
                                ta.select();
                                try {
                                    document.execCommand('copy');
                                    callback();
                                } catch(err){}
                                document.body.removeChild(ta);
                            }
                            return;
                        }

                        var delBtn = e.target.closest('.kcn-delete-btn');
                        if (delBtn) {
                            e.preventDefault();
                            var noteId = delBtn.getAttribute('data-id');
                            var card = delBtn.closest('.kcn-note');
                            if (!noteId || !card) return;

                            delBtn.disabled = true;
                            delBtn.textContent = 'در حال حذف...';

                            postAjax('kcn_delete_note', {note_id: noteId}).then(function(res){
                                if (res && res.success) {
                                    card.remove();
                                } else {
                                    delBtn.disabled = false;
                                    delBtn.textContent = 'حذف';
                                }
                            }).catch(function(){
                                delBtn.disabled = false;
                                delBtn.textContent = 'حذف';
                            });
                            return;
                        }

                        var favBtn = e.target.closest('.kcn-fav-btn');
                        if (favBtn) {
                            e.preventDefault();
                            var noteId = favBtn.getAttribute('data-id');
                            var card = favBtn.closest('.kcn-note');
                            if (!noteId || !card) return;

                            postAjax('kcn_toggle_fav', {note_id: noteId}).then(function(res){
                                if (res && res.success) {
                                    if (parseInt(res.data.fav, 10) === 1) {
                                        card.classList.add('fav');
                                        favBtn.textContent = '★ اوکیه';
                                    } else {
                                        card.classList.remove('fav');
                                        favBtn.textContent = '☆ علامت بزن';
                                    }
                                }
                            });
                            return;
                        }

                        var editBtn = e.target.closest('.kcn-edit-title-btn');
                        if (editBtn) {
                            e.preventDefault();
                            var note = editBtn.closest('.kcn-note');
                            if (!note) return;
                            var editBox = note.querySelector('.kcn-save-title-wrap');
                            if (editBox) editBox.classList.toggle('open');
                            return;
                        }

                        var saveTitleBtn = e.target.closest('.kcn-save-title-btn');
                        if (saveTitleBtn) {
                            e.preventDefault();
                            var note = saveTitleBtn.closest('.kcn-note');
                            if (!note) return;

                            var noteId = saveTitleBtn.getAttribute('data-id');
                            var input = note.querySelector('.kcn-note-name-input');
                            var titleEl = note.querySelector('.kcn-note-name');
                            var statusEl = note.querySelector('.kcn-status');
                            var editBox = note.querySelector('.kcn-save-title-wrap');

                            if (!noteId || !input || !titleEl) return;

                            var newTitle = (input.value || '').trim();
                            if (!newTitle) return;

                            saveTitleBtn.disabled = true;
                            saveTitleBtn.textContent = 'در حال ذخیره...';

                            postAjax('kcn_update_title', {
                                note_id: noteId,
                                title: newTitle
                            }).then(function(res){
                                saveTitleBtn.disabled = false;
                                saveTitleBtn.textContent = 'ذخیره عنوان';

                                if (res && res.success) {
                                    titleEl.textContent = res.data.title;
                                    if (statusEl) {
                                        statusEl.textContent = 'عنوان ذخیره شد';
                                        statusEl.classList.add('show');
                                        setTimeout(function(){
                                            statusEl.classList.remove('show');
                                        }, 1500);
                                    }
                                    if (editBox) editBox.classList.remove('open');
                                }
                            }).catch(function(){
                                saveTitleBtn.disabled = false;
                                saveTitleBtn.textContent = 'ذخیره عنوان';
                            });
                            return;
                        }

                        var bulkDelBtn = e.target.closest('.kcn-bulk-delete-btn');
                        if (bulkDelBtn) {
                            e.preventDefault();
                            var checked = Array.prototype.slice.call(document.querySelectorAll('.kcn-bulk-check:checked'));
                            var ids = checked.map(function(ch){ return ch.value; });

                            if (!ids.length) return;

                            bulkDelBtn.disabled = true;
                            bulkDelBtn.textContent = 'در حال حذف...';

                            postAjax('kcn_bulk_delete', {ids: ids}).then(function(res){
                                bulkDelBtn.disabled = false;
                                bulkDelBtn.textContent = 'حذف انتخاب‌شده‌ها';

                                if (res && res.success) {
                                    ids.forEach(function(id){
                                        var card = document.querySelector('.kcn-note[data-id="' + id + '"]');
                                        if (card) card.remove();
                                    });
                                }
                            }).catch(function(){
                                bulkDelBtn.disabled = false;
                                bulkDelBtn.textContent = 'حذف انتخاب‌شده‌ها';
                            });
                            return;
                        }
                    });
                });
            </script>
            <?php
        }

        public function render_app() {
            $this->handle_submit();
            $notes = $this->get_notes();

            usort($notes, function($a, $b){
                $af = !empty($a['fav']) ? 1 : 0;
                $bf = !empty($b['fav']) ? 1 : 0;
                if ($af !== $bf) {
                    return $bf - $af;
                }
                return strcmp($b['created'], $a['created']);
            });

            ob_start();
            $this->render_styles();
            $this->render_script();
            ?>
            <div class="kcn-wrap">

                <div class="kcn-card">
                    <h2 class="kcn-title">ارسال کد جدید</h2>

                    <form method="post">
                        <div class="kcn-field">
                            <label class="kcn-label">عنوان</label>
                            <input class="kcn-input" type="text" name="kcn_title" placeholder="مثلاً: کد فرم تماس" required>
                        </div>

                        <div class="kcn-field">
                            <label class="kcn-label">زبان</label>
                            <select class="kcn-select" name="kcn_language">
                                <option value="text">Text</option>
                                <option value="php">PHP</option>
                                <option value="js">JavaScript</option>
                                <option value="html">HTML</option>
                                <option value="css">CSS</option>
                                <option value="json">JSON</option>
                                <option value="sql">SQL</option>
                                <option value="python">Python</option>
                            </select>
                        </div>

                        <div class="kcn-field">
                            <label class="kcn-label">کد</label>
                            <textarea class="kcn-textarea" name="kcn_code" placeholder="کد را اینجا وارد کنید..." required></textarea>
                        </div>

                        <input type="hidden" name="kcn_action" value="save_note">
                        <?php wp_nonce_field('kcn_save_note', 'kcn_nonce'); ?>

                        <button type="submit" class="kcn-btn kcn-btn-primary">ذخیره کد</button>
                    </form>
                </div>

                <div class="kcn-card">
                    <div class="kcn-toolbar">
                        <button type="button" class="kcn-btn kcn-btn-danger kcn-bulk-delete-btn">حذف انتخاب‌شده‌ها</button>
                    </div>

                    <h2 class="kcn-title">لیست کدهای ذخیره‌شده</h2>

                    <?php if (empty($notes)) : ?>
                        <div class="kcn-empty">هنوز کدی ذخیره نشده است.</div>
                    <?php else : ?>
                        <div class="kcn-list">
                            <?php foreach ($notes as $index => $note) :
                                $code_id = 'kcn_code_' . md5($note['id'] . '_' . $index);
                                $full_id = 'kcn_full_' . md5($note['id'] . '_full_' . $index);
                                $is_fav = !empty($note['fav']);
                            ?>
                                <div class="kcn-note <?php echo $is_fav ? 'fav' : ''; ?>" data-id="<?php echo esc_attr($note['id']); ?>">
                                    <div class="kcn-note-head">
                                        <div class="kcn-row-top">
                                            <div class="kcn-check">
                                                <input type="checkbox" class="kcn-bulk-check" value="<?php echo esc_attr($note['id']); ?>">
                                            </div>

                                            <div class="kcn-main">
                                                <div class="kcn-note-name"><?php echo esc_html($note['title']); ?></div>

                                                <div class="kcn-save-title-wrap">
                                                    <input type="text" class="kcn-note-name-input" value="<?php echo esc_attr($note['title']); ?>">
                                                    <button type="button" class="kcn-btn kcn-btn-light kcn-save-title-btn" data-id="<?php echo esc_attr($note['id']); ?>">ذخیره عنوان</button>
                                                </div>

                                                <div class="kcn-status"></div>

                                                <div class="kcn-note-sub">
                                                    <?php echo esc_html(strtoupper($note['language'])); ?>
                                                    -
                                                    <?php echo esc_html($note['created']); ?>
                                                </div>

                                                <div class="kcn-preview"><?php echo esc_html($note['code']); ?></div>

                                                <div id="<?php echo esc_attr($full_id); ?>" class="kcn-full">
                                                    <pre id="<?php echo esc_attr($code_id); ?>" class="kcn-code"><?php echo esc_html($note['code']); ?></pre>
                                                </div>

                                                <div class="kcn-actions">
                                                    <button type="button" class="kcn-btn kcn-btn-light kcn-toggle-btn" data-target="<?php echo esc_attr($full_id); ?>">نمایش بیشتر</button>
                                                    <button type="button" class="kcn-btn kcn-btn-light kcn-copy-btn" data-target="<?php echo esc_attr($code_id); ?>">کپی کل کد</button>
                                                    <button type="button" class="kcn-btn kcn-btn-light kcn-edit-title-btn">ویرایش عنوان</button>
                                                    <button type="button" class="kcn-btn kcn-btn-star kcn-fav-btn" data-id="<?php echo esc_attr($note['id']); ?>">
                                                        <?php echo $is_fav ? '★ اوکیه' : '☆ علامت بزن'; ?>
                                                    </button>
                                                    <button type="button" class="kcn-btn kcn-btn-danger kcn-delete-btn" data-id="<?php echo esc_attr($note['id']); ?>">حذف</button>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            <?php endforeach; ?>
                        </div>
                    <?php endif; ?>
                </div>

            </div>
            <?php
            return ob_get_clean();
        }
    }

    new KCN_Keep_Like_Code_App_V2();
}



add_action('wp_footer', function() {
    echo '
    <style>
        .rubika-wrap {
            position: fixed;
            right: 15px;
            bottom: 15px;
            width: 100px;
            z-index: 99999;
        }

        .rubika-float {
            display: block;
            width: 100%;
            background: none;
            border: none;
            border-radius: 0;
            box-shadow: none;
            padding: 0;
            transition: transform 0.2s ease-in-out;
        }

        .rubika-float:hover {
            transform: scale(1.05);
        }

        .rubika-float img {
            width: 100%;
            height: auto;
            display: block;
        }

        .rubika-close {
            position: absolute;
            top: -20px;
            left: 0;

            width: auto !important;
            height: auto !important;
            min-width: 0 !important;
            min-height: 0 !important;

            padding: 0 !important;
            margin: 0 !important;

            background: transparent !important;
            border: none !important;
            box-shadow: none !important;
            outline: none !important;

            color: #000 !important;
            font-size: 26px !important;
            font-weight: bold;
            line-height: 1 !important;

            cursor: pointer;
            z-index: 100000;

            appearance: none;
            -webkit-appearance: none;
        }

        .rubika-close:hover,
        .rubika-close:focus,
        .rubika-close:active {
            background: transparent !important;
            border: none !important;
            box-shadow: none !important;
            outline: none !important;
            color: #000 !important;
        }
    </style>

    <div class="rubika-wrap" id="rubikaWrap">
        <button type="button" class="rubika-close" id="rubikaClose" aria-label="بستن">×</button>

        <a href="https://rubika.ir/faryazan365" class="rubika-float" target="_blank" rel="noopener noreferrer">
            <img src="https://faryazandecor.com/wp-content/uploads/2026/03/logo01@3x-2.png" alt="Rubika">
        </a>
    </div>

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var closeBtn = document.getElementById("rubikaClose");
            var rubikaWrap = document.getElementById("rubikaWrap");

            if (closeBtn && rubikaWrap) {
                closeBtn.addEventListener("click", function() {
                    rubikaWrap.style.display = "none";
                });
            }
        });
    </script>
    ';
});


if (!defined('ABSPATH')) exit;

/**
 * فقط ادمین
 */
function qv_is_admin_user() {
    return current_user_can('manage_woocommerce') || current_user_can('administrator');
}

/**
 * لیبل attribute
 */
function qv_get_attribute_label_safe($name, $product = null) {
    if (function_exists('wc_attribute_label')) {
        $label = wc_attribute_label($name, $product);
        if (!empty($label)) return $label;
    }

    if (strpos($name, 'pa_') === 0) {
        $name = str_replace('pa_', '', $name);
    }

    return ucfirst(str_replace(array('-', '_'), ' ', $name));
}

/**
 * متن خوانا برای option
 */
function qv_get_readable_option_label($attribute_name, $option_value) {
    if ($option_value === '' || $option_value === null) {
        return '';
    }

    if (taxonomy_exists($attribute_name)) {
        $term = get_term_by('slug', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }

        $term = get_term_by('name', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }
    }

    $decoded = rawurldecode($option_value);
    $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8');
    return $decoded;
}

/**
 * همه attributeهای قابل انتخاب
 * - هم attributeهای روی خود محصول
 * - هم همه attributeهای سراسری ووکامرس
 */
function qv_get_all_selectable_attributes($product) {
    $result = array();
    $map = array();

    /**
     * 1) اول attributeهای خود محصول
     */
    $product_attributes = $product->get_attributes();

    if (!empty($product_attributes)) {
        foreach ($product_attributes as $attribute_key => $attribute_obj) {
            if (!is_a($attribute_obj, 'WC_Product_Attribute')) {
                continue;
            }

            $attribute_name = $attribute_obj->get_name();
            $label = qv_get_attribute_label_safe($attribute_name, $product);
            $options = array();

            if ($attribute_obj->is_taxonomy()) {
                $terms = wc_get_product_terms($product->get_id(), $attribute_name, array('fields' => 'all'));

                if (!empty($terms) && !is_wp_error($terms)) {
                    foreach ($terms as $term) {
                        $options[] = array(
                            'value' => $term->slug,
                            'label' => $term->name,
                        );
                    }
                }
            } else {
                $raw_options = $attribute_obj->get_options();

                if (!empty($raw_options)) {
                    foreach ($raw_options as $opt) {
                        if ($opt === '' || $opt === null) continue;

                        $options[] = array(
                            'value' => $opt,
                            'label' => qv_get_readable_option_label($attribute_name, $opt),
                        );
                    }
                }
            }

            if (!isset($map[$attribute_name])) {
                $map[$attribute_name] = array(
                    'name'    => $attribute_name,
                    'label'   => $label,
                    'options' => array(),
                );
            }

            foreach ($options as $opt) {
                $map[$attribute_name]['options'][(string)$opt['value']] = $opt;
            }
        }
    }

    /**
     * 2) همه attributeهای سراسری ووکامرس
     */
    $global_attributes = function_exists('wc_get_attribute_taxonomies') ? wc_get_attribute_taxonomies() : array();

    if (!empty($global_attributes)) {
        foreach ($global_attributes as $ga) {
            if (empty($ga->attribute_name)) continue;

            $taxonomy = wc_attribute_taxonomy_name($ga->attribute_name);
            if (!taxonomy_exists($taxonomy)) continue;

            $label = !empty($ga->attribute_label) ? $ga->attribute_label : qv_get_attribute_label_safe($taxonomy, $product);

            if (!isset($map[$taxonomy])) {
                $map[$taxonomy] = array(
                    'name'    => $taxonomy,
                    'label'   => $label,
                    'options' => array(),
                );
            }

            $terms = get_terms(array(
                'taxonomy'   => $taxonomy,
                'hide_empty' => false,
            ));

            if (!empty($terms) && !is_wp_error($terms)) {
                foreach ($terms as $term) {
                    $map[$taxonomy]['options'][(string)$term->slug] = array(
                        'value' => $term->slug,
                        'label' => $term->name,
                    );
                }
            }
        }
    }

    foreach ($map as $attribute_name => $item) {
        if (!empty($item['options'])) {
            $item['options'] = array_values($item['options']);
            $result[] = $item;
        }
    }

    return $result;
}

/**
 * تبدیل محصول به variable
 */
function qv_ensure_variable_product($product_id) {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    if ($product->is_type('variable')) {
        return true;
    }

    wp_set_object_terms($product_id, 'variable', 'product_type');
    clean_post_cache($product_id);

    $product = wc_get_product($product_id);
    return ($product && $product->is_type('variable'));
}

/**
 * افزودن attribute به محصول اگر نبود
 */
function qv_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    $attributes = $product->get_attributes();

    if (isset($attributes[$attribute_name])) {
        $attr_obj = $attributes[$attribute_name];

        if (is_a($attr_obj, 'WC_Product_Attribute')) {
            $attr_obj->set_visible(true);
            $attr_obj->set_variation(true);

            if (!$attr_obj->is_taxonomy() && $attribute_value !== '') {
                $options = $attr_obj->get_options();
                if (!in_array($attribute_value, $options, true)) {
                    $options[] = $attribute_value;
                    $attr_obj->set_options($options);
                }
            }

            $attributes[$attribute_name] = $attr_obj;
            $product->set_attributes($attributes);
            $product->save();
        }

        return true;
    }

    $new_attr = new WC_Product_Attribute();

    if (taxonomy_exists($attribute_name)) {
        $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0;
        $new_attr->set_id($taxonomy_id);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options(array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    } else {
        $new_attr->set_id(0);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    }

    $attributes[$attribute_name] = $new_attr;
    $product->set_attributes($attributes);
    $product->save();

    return true;
}

/**
 * variation تکراری
 */
function qv_variation_exists($product_id, $variation_attributes) {
    $children = get_posts(array(
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'post_status' => array('publish', 'private'),
        'numberposts' => -1,
        'fields'      => 'ids',
    ));

    if (empty($children)) return false;

    foreach ($children as $variation_id) {
        $same = true;

        foreach ($variation_attributes as $key => $value) {
            $existing = get_post_meta($variation_id, $key, true);
            if ((string)$existing !== (string)$value) {
                $same = false;
                break;
            }
        }

        if ($same) {
            return true;
        }
    }

    return false;
}

/**
 * فرم
 */
function qv_render_quick_variation_form() {
    if (!is_product()) return;
    if (!qv_is_admin_user()) return;

    global $product;
    if (!$product || !is_a($product, 'WC_Product')) return;

    $attributes = qv_get_all_selectable_attributes($product);
    if (empty($attributes)) return;
    ?>
    <div class="qv-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;">
        <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3>

        <form method="post" class="qv-quick-variation-form" autocomplete="off" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
            <?php wp_nonce_field('qv_quick_variation_action', 'qv_quick_variation_nonce'); ?>
            <input type="hidden" name="qv_product_id" value="<?php echo esc_attr($product->get_id()); ?>">

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی اول</label>
                <select name="qv_attr1" id="qv_attr1_custom" style="width:100%;padding:8px;">
                    <option value="">انتخاب ویژگی</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label>
                <select name="qv_val1" id="qv_val1_custom" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی دوم</label>
                <select name="qv_attr2" id="qv_attr2_custom" style="width:100%;padding:8px;">
                    <option value="">بدون ویژگی دوم</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label>
                <select name="qv_val2" id="qv_val2_custom" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div style="grid-column:1/-1;">
                <label style="display:block;margin-bottom:6px;">قیمت</label>
                <input type="number" step="0.01" min="0" name="qv_price" required style="width:100%;padding:8px;">
            </div>

            <div style="grid-column:1/-1;">
                <button type="submit" name="qv_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;">
                    افزودن تنوع
                </button>
            </div>
        </form>
    </div>

    <script>
    (function(){
        var attributes = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;

        var attr1 = document.getElementById('qv_attr1_custom');
        var val1  = document.getElementById('qv_val1_custom');
        var attr2 = document.getElementById('qv_attr2_custom');
        var val2  = document.getElementById('qv_val2_custom');

        if (!attr1 || !val1 || !attr2 || !val2) return;

        function findAttribute(name) {
            for (var i = 0; i < attributes.length; i++) {
                if (attributes[i].name === name) return attributes[i];
            }
            return null;
        }

        function fillValues(attrSelect, valueSelect) {
            var attrName = attrSelect.value;
            var previousValue = valueSelect.value || '';

            valueSelect.innerHTML = '';

            if (!attrName) {
                var p = document.createElement('option');
                p.value = '';
                p.textContent = 'ابتدا ویژگی را انتخاب کنید';
                valueSelect.appendChild(p);
                return;
            }

            var data = findAttribute(attrName);

            var first = document.createElement('option');
            first.value = '';
            first.textContent = 'انتخاب مقدار';
            valueSelect.appendChild(first);

            var any = document.createElement('option');
            any.value = '__any__';
            any.textContent = 'همه موارد';
            valueSelect.appendChild(any);

            if (data && data.options) {
                data.options.forEach(function(opt){
                    var option = document.createElement('option');
                    option.value = opt.value;
                    option.textContent = opt.label;
                    valueSelect.appendChild(option);
                });
            }

            if (previousValue) {
                var exists = false;
                for (var i = 0; i < valueSelect.options.length; i++) {
                    if (valueSelect.options[i].value === previousValue) {
                        exists = true;
                        break;
                    }
                }
                valueSelect.value = exists ? previousValue : '';
            }
        }

        attr1.addEventListener('change', function(e){
            e.stopPropagation();
            fillValues(attr1, val1);

            if (attr2.value && attr2.value === attr1.value) {
                attr2.value = '';
                fillValues(attr2, val2);
            }
        }, true);

        attr2.addEventListener('change', function(e){
            e.stopPropagation();

            if (attr1.value && attr2.value && attr1.value === attr2.value) {
                alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.');
                attr2.value = '';
            }

            fillValues(attr2, val2);
        }, true);

        val1.addEventListener('change', function(e){
            e.stopPropagation();
        }, true);

        val2.addEventListener('change', function(e){
            e.stopPropagation();
        }, true);

        attr1.addEventListener('click', function(e){ e.stopPropagation(); }, true);
        attr2.addEventListener('click', function(e){ e.stopPropagation(); }, true);
        val1.addEventListener('click', function(e){ e.stopPropagation(); }, true);
        val2.addEventListener('click', function(e){ e.stopPropagation(); }, true);
    })();
    </script>
    <?php
}
add_action('woocommerce_after_single_product_summary', 'qv_render_quick_variation_form', 5);

/**
 * ثبت فرم
 */
function qv_handle_quick_variation_submit() {
    if (!isset($_POST['qv_quick_variation_submit'])) return;
    if (!qv_is_admin_user()) return;

    if (!isset($_POST['qv_quick_variation_nonce']) || !wp_verify_nonce($_POST['qv_quick_variation_nonce'], 'qv_quick_variation_action')) {
        return;
    }

    $product_id = isset($_POST['qv_product_id']) ? absint($_POST['qv_product_id']) : 0;
    $attr1      = isset($_POST['qv_attr1']) ? wc_clean(wp_unslash($_POST['qv_attr1'])) : '';
    $val1       = isset($_POST['qv_val1']) ? wc_clean(wp_unslash($_POST['qv_val1'])) : '';
    $attr2      = isset($_POST['qv_attr2']) ? wc_clean(wp_unslash($_POST['qv_attr2'])) : '';
    $val2       = isset($_POST['qv_val2']) ? wc_clean(wp_unslash($_POST['qv_val2'])) : '';
    $price      = isset($_POST['qv_price']) ? wc_format_decimal(wp_unslash($_POST['qv_price'])) : '';

    if (!$product_id || !$attr1 || $val1 === '' || $price === '') {
        wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error');
        return;
    }

    if ($attr2 && !$val2 && $val2 !== '__any__') {
        wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error');
        return;
    }

    if ($attr1 && $attr2 && $attr1 === $attr2) {
        wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error');
        return;
    }

    if ($val1 === '__any__' && $val2 === '__any__') {
        wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان همه موارد را انتخاب کرد.', 'error');
        return;
    }

    if (!qv_ensure_variable_product($product_id)) {
        wc_add_notice('تبدیل محصول به variable ناموفق بود.', 'error');
        return;
    }

    qv_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : '');
    if ($attr2) {
        qv_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : '');
    }

    $variation_attributes = array(
        'attribute_' . $attr1 => ($val1 === '__any__' ? '' : $val1),
    );

    if ($attr2) {
        $variation_attributes['attribute_' . $attr2] = ($val2 === '__any__' ? '' : $val2);
    }

    if (qv_variation_exists($product_id, $variation_attributes)) {
        wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error');
        return;
    }

    $variation_post = array(
        'post_title'  => 'Product variation',
        'post_name'   => 'product-' . $product_id . '-variation',
        'post_status' => 'publish',
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'guid'        => home_url('/?product_variation=product-' . $product_id . '-variation'),
    );

    $variation_id = wp_insert_post($variation_post);

    if (!$variation_id || is_wp_error($variation_id)) {
        wc_add_notice('ساخت variation ناموفق بود.', 'error');
        return;
    }

    foreach ($variation_attributes as $meta_key => $meta_value) {
        update_post_meta($variation_id, $meta_key, $meta_value);
    }

    update_post_meta($variation_id, '_regular_price', $price);
    update_post_meta($variation_id, '_price', $price);

    $variation = new WC_Product_Variation($variation_id);
    $variation->set_parent_id($product_id);
    $variation->set_regular_price($price);
    $variation->set_price($price);

    $set_attrs = array(
        $attr1 => ($val1 === '__any__' ? '' : $val1),
    );

    if ($attr2) {
        $set_attrs[$attr2] = ($val2 === '__any__' ? '' : $val2);
    }

    $variation->set_attributes($set_attrs);
    $variation->save();

    WC_Product_Variable::sync($product_id);
    wc_delete_product_transients($product_id);

    wc_add_notice('تنوع جدید با موفقیت ساخته شد.', 'success');
}
add_action('init', 'qv_handle_quick_variation_submit');


add_action('template_redirect', 'fz_bed_vertical_price_date_start_buffer', 0);

add_filter('pre_get_document_title', 'fz_bed_vertical_price_date_filter_text', 999999);
add_filter('document_title_parts', 'fz_bed_vertical_price_date_document_parts', 999999);
add_filter('rank_math/frontend/title', 'fz_bed_vertical_price_date_filter_text', 999999);
add_filter('rank_math/opengraph/facebook/title', 'fz_bed_vertical_price_date_filter_text', 999999);
add_filter('rank_math/opengraph/twitter/title', 'fz_bed_vertical_price_date_filter_text', 999999);
add_filter('woocommerce_page_title', 'fz_bed_vertical_price_date_filter_text', 999999);
add_filter('single_term_title', 'fz_bed_vertical_price_date_filter_text', 999999);

function fz_bed_vertical_price_date_is_target() {
    if (is_admin()) {
        return false;
    }

    return function_exists('is_product_category') && is_product_category();
}

function fz_bed_vertical_price_date_start_buffer() {
    if (!fz_bed_vertical_price_date_is_target()) {
        return;
    }

    ob_start('fz_bed_vertical_price_date_replace_html');
}

function fz_bed_vertical_price_date_document_parts($parts) {
    if (!fz_bed_vertical_price_date_is_target()) {
        return $parts;
    }

    if (isset($parts['title'])) {
        $parts['title'] = fz_bed_vertical_price_date_filter_text($parts['title']);
    }

    return $parts;
}

function fz_bed_vertical_price_date_filter_text($text) {
    if (!fz_bed_vertical_price_date_is_target()) {
        return $text;
    }

    return fz_bed_vertical_price_date_add_suffix($text);
}

function fz_bed_vertical_price_date_replace_html($html) {
    if (!fz_bed_vertical_price_date_is_target()) {
        return $html;
    }

    $html = preg_replace_callback(
        '/<title\b[^>]*>(.*?)<\/title>/is',
        function ($m) {
            return '<title>' . esc_html(fz_bed_vertical_price_date_add_suffix($m[1])) . '</title>';
        },
        $html,
        1
    );

    $html = preg_replace_callback(
        '/<h1\b([^>]*)>(.*?)<\/h1>/is',
        function ($m) {
            return '<h1' . $m[1] . '>' . esc_html(fz_bed_vertical_price_date_add_suffix(wp_strip_all_tags($m[2]))) . '</h1>';
        },
        $html,
        1
    );

    $html = preg_replace_callback(
        '/<meta\b[^>]*>/is',
        function ($m) {
            $tag = $m[0];

            if (
                stripos($tag, 'og:title') === false &&
                stripos($tag, 'twitter:title') === false
            ) {
                return $tag;
            }

            if (!preg_match('/content=(["\'])(.*?)\1/is', $tag, $cm)) {
                return $tag;
            }

            $new_content = esc_attr(
                fz_bed_vertical_price_date_add_suffix(
                    wp_strip_all_tags($cm[2])
                )
            );

            return preg_replace(
                '/content=(["\'])(.*?)\1/is',
                'content="' . $new_content . '"',
                $tag,
                1
            );
        },
        $html
    );

    return $html;
}

function fz_bed_vertical_price_date_add_suffix($text) {
    $text = trim(wp_strip_all_tags($text));

    $suffix = ' + قیمت روز ' . fz_bed_vertical_price_date_today_jalali();

    if (mb_strpos($text, $suffix) !== false) {
        return $text;
    }

    return $text . $suffix;
}

function fz_bed_vertical_price_date_today_jalali() {
    $timestamp = current_time('timestamp');

    $gy = (int) date('Y', $timestamp);
    $gm = (int) date('n', $timestamp);
    $gd = (int) date('j', $timestamp);

    list($jy, $jm, $jd) = fz_bed_vertical_price_date_gregorian_to_jalali($gy, $gm, $gd);

    $months = array(
        1  => 'فروردین',
        2  => 'اردیبهشت',
        3  => 'خرداد',
        4  => 'تیر',
        5  => 'مرداد',
        6  => 'شهریور',
        7  => 'مهر',
        8  => 'آبان',
        9  => 'آذر',
        10 => 'دی',
        11 => 'بهمن',
        12 => 'اسفند',
    );

    return fz_bed_vertical_price_date_fa_num($jd) . ' ' . $months[$jm];
}

function fz_bed_vertical_price_date_fa_num($num) {
    return str_replace(
        array('0','1','2','3','4','5','6','7','8','9'),
        array('۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'),
        (string) $num
    );
}

function fz_bed_vertical_price_date_gregorian_to_jalali($gy, $gm, $gd) {
    $g_d_m = array(0,31,59,90,120,151,181,212,243,273,304,334);

    if ($gy > 1600) {
        $jy = 979;
        $gy -= 1600;
    } else {
        $jy = 0;
        $gy -= 621;
    }

    $gy2 = ($gm > 2) ? ($gy + 1) : $gy;

    $days = 365 * $gy
        + intval(($gy2 + 3) / 4)
        - intval(($gy2 + 99) / 100)
        + intval(($gy2 + 399) / 400)
        - 80
        + $gd
        + $g_d_m[$gm - 1];

    $jy += 33 * intval($days / 12053);
    $days %= 12053;

    $jy += 4 * intval($days / 1461);
    $days %= 1461;

    if ($days > 365) {
        $jy += intval(($days - 1) / 365);
        $days = ($days - 1) % 365;
    }

    if ($days < 186) {
        $jm = 1 + intval($days / 31);
        $jd = 1 + ($days % 31);
    } else {
        $jm = 7 + intval(($days - 186) / 30);
        $jd = 1 + (($days - 186) % 30);
    }

    return array($jy, $jm, $jd);
}

/**
 * Front-end Price Editor (Simple + Variable) - Code Snippets
 * ✅ فقط قیمت عادی Regular
 * ✅ مناسب LiteSpeed Cache: بعد از تغییر قیمت، کش همان محصول پاک می‌شود
 */

if ( ! defined('ABSPATH') ) exit;

/** Optional: remove "choose an option" placeholder in variation dropdowns */
add_filter('woocommerce_dropdown_variation_attribute_options_args', function($args){
    $args['show_option_none'] = false;
    return $args;
});

/** Convert Persian/Arabic digits to English + keep only digits */
function fpe_digits_only($val){
    $val = (string) $val;
    $map = [
        '۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9',
        '٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9',
    ];
    $val = strtr($val, $map);
    return preg_replace('/\D+/', '', $val);
}

/** Purge product cache after price update */
function fpe_purge_product_cache($product_id){
    $product_id = absint($product_id);
    if ( ! $product_id ) return;

    if ( function_exists('wc_delete_product_transients') ) {
        wc_delete_product_transients($product_id);
    }

    clean_post_cache($product_id);

    if ( function_exists('wc_get_product') ) {
        $product = wc_get_product($product_id);

        if ( $product && $product->is_type('variation') ) {
            $parent_id = $product->get_parent_id();
            if ( $parent_id ) {
                wc_delete_product_transients($parent_id);
                clean_post_cache($parent_id);

                do_action('litespeed_purge_post', $parent_id);
                do_action('litespeed_purge_url', get_permalink($parent_id));
            }
        }
    }

    do_action('litespeed_purge_post', $product_id);
    do_action('litespeed_purge_url', get_permalink($product_id));
}

/** Variation label helpers */
function fpe_attribute_label($attr_key, $parent_product){
    $key = preg_replace('/^attribute_/', '', (string)$attr_key);

    if (strpos($key, 'pa_') === 0 && taxonomy_exists($key)) {
        $tax = get_taxonomy($key);
        if ($tax && ! empty($tax->labels->singular_name)) return $tax->labels->singular_name;
        return wc_attribute_label($key, $parent_product);
    }

    $label = wc_attribute_label($key, $parent_product);
    if ($label && $label !== $key) return $label;

    return str_replace(['pa_', '-', '_'], ['', ' ', ' '], $key);
}

function fpe_attribute_value_readable($taxonomy_or_name, $raw_val){
    $raw_val = (string)$raw_val;
    $decoded = rawurldecode($raw_val);
    $tax = preg_replace('/^attribute_/', '', (string)$taxonomy_or_name);

    if (strpos($tax, 'pa_') === 0 && taxonomy_exists($tax)) {
        $term = get_term_by('slug', $raw_val, $tax);
        if ( ! $term || is_wp_error($term) ) $term = get_term_by('slug', $decoded, $tax);
        if ( ! $term || is_wp_error($term) ) $term = get_term_by('name', $decoded, $tax);
        if ( $term && ! is_wp_error($term) ) return $term->name;
        return $decoded;
    }

    return $decoded;
}

function fpe_get_variation_label($variation, $parent_product){
    $out = [];
    foreach ((array)$variation->get_attributes() as $k => $v) {
        if ($v === '' || $v === null) continue;
        $out[] = fpe_attribute_label($k, $parent_product) . ': ' . fpe_attribute_value_readable($k, $v);
    }
    return $out ? implode(' | ', $out) : ('تنوع #' . $variation->get_id());
}

/** UI */
add_action('woocommerce_after_add_to_cart_form', function () {

    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( ! is_user_logged_in() ) return;
    if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) return;

    global $product;
    if ( ! $product ) return;

    $type = $product->get_type();
    if ( $type !== 'simple' && $type !== 'variable' ) return;

    $product_id = $product->get_id();
    $nonce = wp_create_nonce('fpe_save_prices');
    $title = ($type === 'simple') ? 'ویرایش قیمت محصول ساده' : 'ویرایش قیمت تنوع‌ها';

    echo '<style>
    .fpe-wrap{margin:16px 0;}
    .fpe-details{border:1px solid #e5e7eb;border-radius:14px;background:#fafafa;overflow:hidden;}
    .fpe-details>summary{list-style:none;cursor:pointer;padding:12px;display:flex;align-items:center;gap:10px;user-select:none;}
    .fpe-details>summary::-webkit-details-marker{display:none;}
    .fpe-badge{font-size:12px;padding:4px 10px;border-radius:999px;background:#111;color:#fff;white-space:nowrap;}
    .fpe-title{font-size:14px;font-weight:900;line-height:1.4;margin:0;flex:1;}
    .fpe-hint{font-size:12px;opacity:.7;margin:0;}
    .fpe-body{padding:12px;}

    .fpe-grid-head{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:10px;border-bottom:1px solid #e5e7eb;font-size:13px;font-weight:900;background:#f3f4f6;border-radius:12px;margin-bottom:10px;}
    .fpe-row{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:12px 10px;border:1px solid #e5e7eb;border-radius:14px;align-items:center;margin-bottom:10px;background:#fff;}
    .fpe-rows .fpe-row:nth-child(even){ background:#eef6ff; }

    .fpe-attr{font-size:13px;line-height:1.6;word-break:break-word;font-weight:800;}

    .fpe-input{
      width:100%;padding:10px;border:1px solid #c3c4c7;border-radius:10px;font-size:16px;outline:none;background:#fff;
      direction:ltr;text-align:center;
    }
    .fpe-input:focus{border-color:#2271b1; box-shadow:0 0 0 1px #2271b1;}

    .fpe-actions{margin-top:12px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;}
    .fpe-note{font-size:12px;opacity:.75;margin:0;}

    .fpe-btn{width:100%;padding:12px 18px;border:1px solid #2271b1;border-radius:6px;cursor:pointer;font-size:14px;font-weight:700;background:#2271b1;color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.08);}
    .fpe-btn:hover{background:#135e96;border-color:#135e96;}
    .fpe-btn:active{background:#0a4b78;border-color:#0a4b78;transform:translateY(1px);}

    @media (max-width:680px){
      .fpe-grid-head{display:none;}
      .fpe-row{grid-template-columns:1fr;gap:10px;padding:12px;}
      .fpe-field{display:flex;flex-direction:column;gap:6px;}
      .fpe-label{font-size:12px;opacity:.75;}
      .fpe-attr{font-size:14px;}
    }
    @media (min-width:681px){
      .fpe-btn{width:auto;min-width:220px;}
      .fpe-label{display:none;}
      .fpe-field{display:block;}
    }
    </style>';

    echo '<div class="fpe-wrap">';
    echo '<details class="fpe-details" '.(isset($_GET["fpe_saved"]) ? "open" : "").'>';
    echo '<summary><span class="fpe-badge">مدیر</span>
            <div style="min-width:0;">
              <p class="fpe-title">'.esc_html($title).'</p>
              <p class="fpe-hint">قیمت‌ها حین تایپ سه‌تایی جدا می‌شوند</p>
            </div>
            <span style="opacity:.65;font-size:18px;">⌄</span>
          </summary>';

    echo '<div class="fpe-body"><form method="post" id="fpe-form">';
    echo '<input type="hidden" name="fpe_product_id" value="'.esc_attr($product_id).'">';
    echo '<input type="hidden" name="fpe_nonce" value="'.esc_attr($nonce).'">';
    echo '<input type="hidden" name="fpe_type" value="'.esc_attr($type).'">';

    echo '<div class="fpe-grid-head"><div>'.($type==='simple'?'محصول':'تنوع').'</div><div>قیمت</div></div>';
    echo '<div class="fpe-rows">';

    if ($type === 'simple') {
        $raw = fpe_digits_only($product->get_regular_price());

        echo '<div class="fpe-row">
                <div class="fpe-attr">این محصول</div>
                <div class="fpe-field">
                  <div class="fpe-label">قیمت</div>
                  <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_simple_regular" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000">
                </div>
              </div>';

    } else {
        foreach ($product->get_children() as $variation_id) {
            $v = wc_get_product($variation_id);
            if (!$v) continue;

            $label = fpe_get_variation_label($v, $product);
            $raw = fpe_digits_only($v->get_regular_price());

            echo '<div class="fpe-row">
                    <div class="fpe-attr">'.esc_html($label).'</div>
                    <div class="fpe-field">
                      <div class="fpe-label">قیمت</div>
                      <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_regular['.esc_attr($variation_id).']" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000">
                    </div>
                  </div>';
        }
    }

    echo '</div>';

    echo '<div class="fpe-actions">
            <button class="fpe-btn" type="submit" name="fpe_save" value="1">به‌روزرسانی</button>
            <p class="fpe-note">بعد از به‌روزرسانی، کش همان محصول پاک می‌شود.</p>
          </div>';

    echo '</form></div></details></div>';

    echo '<script>
    (function(){
      function toEnDigits(s){
        if(!s) return "";
        var map = {"۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
        return String(s).replace(/[۰-۹٠-٩]/g, function(ch){ return map[ch] || ch; });
      }
      function digitsOnly(s){ return toEnDigits(s).replace(/\\D+/g,""); }
      function format3(s){
        s = digitsOnly(s);
        if(!s) return "";
        return s.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ",");
      }
      function caretDigitsIndex(value, caretPos){
        var left = value.slice(0, caretPos);
        return digitsOnly(left).length;
      }
      function caretFromDigitsIndex(formatted, digitIndex){
        var count = 0;
        for (var i=0; i<formatted.length; i++){
          if (/\\d/.test(formatted[i])) count++;
          if (count >= digitIndex) return i+1;
        }
        return formatted.length;
      }

      var inputs = document.querySelectorAll(".fpe-wrap .fpe-price");
      inputs.forEach(function(inp){
        inp.value = format3(inp.value);

        inp.addEventListener("input", function(){
          var oldVal = inp.value;
          var caret = inp.selectionStart || 0;
          var dIndex = caretDigitsIndex(oldVal, caret);
          var newVal = format3(oldVal);
          inp.value = newVal;
          var newCaret = caretFromDigitsIndex(newVal, dIndex);
          try { inp.setSelectionRange(newCaret, newCaret); } catch(err){}
        });

        inp.addEventListener("paste", function(){
          setTimeout(function(){ inp.value = format3(inp.value); }, 0);
        });
      });

      var form = document.getElementById("fpe-form");
      if(form){
        form.addEventListener("submit", function(){
          inputs.forEach(function(inp){ inp.value = digitsOnly(inp.value); });
        });
      }
    })();
    </script>';

}, 50);

/** Save handler */
add_action('template_redirect', function () {

    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( empty($_POST['fpe_save']) ) return;

    if ( ! is_user_logged_in() ) wp_die('Access denied');
    if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) wp_die('Access denied');

    $nonce = isset($_POST['fpe_nonce']) ? sanitize_text_field($_POST['fpe_nonce']) : '';
    if ( ! wp_verify_nonce($nonce, 'fpe_save_prices') ) wp_die('Security check failed');

    $product_id = isset($_POST['fpe_product_id']) ? absint($_POST['fpe_product_id']) : 0;
    if ( ! $product_id ) wp_die('Invalid product');

    $product = wc_get_product($product_id);
    if ( ! $product ) wp_die('Product not found');

    $type = isset($_POST['fpe_type']) ? sanitize_text_field($_POST['fpe_type']) : $product->get_type();

    // SIMPLE
    if ( $type === 'simple' && $product->is_type('simple') ) {
        $new_raw = isset($_POST['fpe_simple_regular']) ? fpe_digits_only(wp_unslash($_POST['fpe_simple_regular'])) : '';
        $old_raw = fpe_digits_only($product->get_regular_price());

        if ($new_raw !== $old_raw) {
            $product->set_regular_price( $new_raw === '' ? '' : $new_raw );
            $product->save();

            fpe_purge_product_cache($product_id);
        }

        wp_safe_redirect( add_query_arg('fpe_saved', '1', get_permalink($product_id)) );
        exit;
    }

    // VARIABLE
    if ( $type === 'variable' && $product->is_type('variable') ) {
        $regulars = (isset($_POST['fpe_regular']) && is_array($_POST['fpe_regular'])) ? $_POST['fpe_regular'] : [];
        $changed_any = false;

        foreach ( $product->get_children() as $variation_id ) {
            if ( ! array_key_exists($variation_id, $regulars) ) continue;

            $v = wc_get_product($variation_id);
            if (!$v) continue;

            $new_raw = fpe_digits_only( wp_unslash($regulars[$variation_id]) );
            $old_raw = fpe_digits_only( $v->get_regular_price() );

            if ($new_raw === $old_raw) continue;

            $v->set_regular_price( $new_raw === '' ? '' : $new_raw );
            $v->save();

            fpe_purge_product_cache($variation_id);

            $changed_any = true;
        }

        if ($changed_any) {
            fpe_purge_product_cache($product_id);
        }

        wp_safe_redirect( add_query_arg('fpe_saved', '1', get_permalink($product_id)) );
        exit;
    }

    wp_die('Unsupported product type');
});

/** Toast */
add_action('wp_footer', function () {
    if ( ! function_exists('is_product') || ! is_product() ) return;

    if ( isset($_GET['fpe_saved']) ) {
        echo '<div id="fpe-toast" style="position:fixed;bottom:18px;left:18px;z-index:999999;background:#111;color:#fff;padding:10px 12px;border-radius:14px;font-size:13px;">به‌روزرسانی انجام شد ✅</div>';
        echo '<script>setTimeout(function(){var t=document.getElementById("fpe-toast"); if(t) t.remove();}, 3200);</script>';
    }
});











کارگری 10
TEXT - 2026-06-05 11:36:08
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage"></small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } * { box-sizing: border-box; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-wrap .summary-card { background: #ffffff; color: #111827; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-wrap .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-wrap .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: none; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } @media (max-width: 380px) { .summary-wrap .summary-card strong { font-size: 14px; } } </style> <script> (function(){ let selectedService = null; let records = []; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const toast = document.getElementById("toast"); const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); } function updatePersonalRecord() { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = ""; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); showToast("ثبت جدید اضافه شد"); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmountValue); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderRecords(); updateSummary(); updatePersonalRecord(); renderFeedback(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="worker-page">

          <div class="page-header">
            <h1 class="page-title">ثبت کار امروز</h1>
            <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
          </div>

          <div class="search-card">
            <label class="search-label">جستجوی خدمت</label>

            <div class="search-input-wrap">
              <div class="search-icon">🔍</div>
              <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
            </div>

            <div class="service-results" id="serviceResults"></div>
          </div>

          <div class="summary-wrap">
            <div class="summary-card">
              <span>مبلغ امروز</span>
              <strong id="todayAmount">۰ تومان</strong>
            </div>

            <div class="summary-card">
              <span>تعداد امروز</span>
              <strong id="todayCount">۰</strong>
            </div>
          </div>

          <div class="personal-record-card">
            <div class="record-icon">🏆</div>
            <div class="record-content">
              <span>رکورد روزانه تو</span>
              <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
              <small id="recordMessage"></small>
            </div>
          </div>

          <div class="feedback-card">
            <div class="feedback-title">آخرین بازخورد عملکرد</div>
            <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
            <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
            <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
          </div>

          <div class="form-card" id="serviceForm">
            <div class="selected-service">
              <span>خدمت انتخاب شده</span>
              <strong id="selectedServiceName">---</strong>
            </div>

            <div class="form-grid">
              <div class="field">
                <label>تعداد</label>
                <input type="number" id="serviceCount" min="1" value="1" />
              </div>

              <div class="field">
                <label>مقدار / مبلغ واحد</label>
                <input type="number" id="servicePrice" min="0" />
              </div>
            </div>

            <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

            <div class="details-box" id="detailsBox">
              <div class="field">
                <label>توضیحات</label>
                <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
              </div>
            </div>

            <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
          </div>

          <div class="records-card">
            <div class="records-title">
              <strong>ثبت‌های امروز</strong>
              <span id="recordsCountText">۰ مورد</span>
            </div>

            <div id="recordsList">
              <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
            </div>
          </div>

        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  * {
    box-sizing: border-box;
  }

  .worker-page {
    max-width: 520px;
    margin: 0 auto;
  }

  .page-header {
    margin-bottom: 14px;
  }

  .page-title {
    font-size: 18px;
    font-weight: 900;
    margin: 0 0 5px;
    color: #111827;
  }

  .page-subtitle {
    font-size: 12px;
    color: #6b7280;
    margin: 0;
    line-height: 1.8;
  }

  .search-card,
  .form-card,
  .records-card {
    background: #ffffff;
    border-radius: 20px;
    padding: 13px;
    box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
    margin-bottom: 13px;
    border: 1px solid #e5e7eb;
  }

  .search-label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    margin-bottom: 8px;
    color: #374151;
  }

  .search-input-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
    background: #f9fafb;
    border: 2px solid #2563eb;
    border-radius: 15px;
    padding: 10px 12px;
  }

  .search-icon {
    font-size: 17px;
  }

  #serviceSearch {
    width: 100%;
    border: none;
    outline: none;
    background: transparent;
    font-size: 14px;
    font-weight: 700;
    color: #111827;
  }

  #serviceSearch::placeholder {
    color: #9ca3af;
    font-weight: 500;
  }

  .service-results {
    margin-top: 10px;
    display: none;
  }

  .service-result-item {
    background: #f8fafc;
    border: 1px solid #e5e7eb;
    border-radius: 13px;
    padding: 10px;
    margin-bottom: 7px;
    cursor: pointer;
  }

  .service-result-item:hover {
    background: #eef2ff;
    border-color: #c7d2fe;
  }

  .service-result-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 3px;
  }

  .service-result-price {
    font-size: 11px;
    color: #6b7280;
  }

  .summary-wrap {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 12px;
  }

  .summary-wrap .summary-card {
    background: #ffffff;
    color: #111827;
    border-radius: 17px;
    padding: 12px;
    box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
    border: 1px solid #e5e7eb;
  }

  .summary-wrap .summary-card span {
    display: block;
    color: #6b7280;
    font-size: 11px;
    font-weight: 700;
    margin-bottom: 6px;
  }

  .summary-wrap .summary-card strong {
    display: block;
    color: #111827;
    font-size: 15px;
    font-weight: 900;
  }

  #todayAmount {
    color: #16a34a;
  }

  .personal-record-card {
    background: linear-gradient(135deg, #fff7ed, #fffbeb);
    border: 1px solid #fed7aa;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    display: flex;
    align-items: center;
    gap: 11px;
    box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
  }

  .record-icon {
    width: 42px;
    height: 42px;
    border-radius: 14px;
    background: #ffedd5;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 21px;
    flex-shrink: 0;
  }

  .record-content {
    flex: 1;
  }

  .record-content span {
    display: block;
    color: #9a3412;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content strong {
    display: block;
    color: #111827;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content small {
    display: none;
    color: #92400e;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.7;
  }

  .feedback-card {
    background: linear-gradient(135deg, #eff6ff, #f8fafc);
    border: 1px solid #bfdbfe;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
  }

  .feedback-title {
    font-size: 12px;
    font-weight: 900;
    color: #1d4ed8;
    margin-bottom: 7px;
  }

  .feedback-main {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 5px;
  }

  .feedback-sub {
    font-size: 11px;
    line-height: 1.8;
    color: #4b5563;
  }

  .feedback-badge {
    display: inline-block;
    margin-top: 8px;
    padding: 5px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 900;
  }

  .feedback-badge.positive {
    background: #dbeafe;
    color: #1d4ed8;
  }

  .feedback-badge.negative {
    background: #fef3c7;
    color: #92400e;
  }

  .feedback-badge.neutral {
    background: #e5e7eb;
    color: #374151;
  }

  .form-card {
    display: none;
  }

  .selected-service {
    background: #eff6ff;
    border: 1px solid #bfdbfe;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 12px;
  }

  .selected-service span {
    display: block;
    color: #1d4ed8;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .selected-service strong {
    display: block;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .field {
    margin-bottom: 10px;
  }

  .field label {
    display: block;
    font-size: 11px;
    font-weight: 900;
    color: #374151;
    margin-bottom: 6px;
  }

  .field input,
  .field textarea {
    width: 100%;
    border: 1px solid #d1d5db;
    outline: none;
    background: #f9fafb;
    border-radius: 13px;
    padding: 10px;
    font-size: 13px;
    font-family: inherit;
  }

  .field input:focus,
  .field textarea:focus {
    border-color: #2563eb;
    background: #ffffff;
  }

  .field textarea {
    min-height: 75px;
    resize: vertical;
    line-height: 1.8;
  }

  .details-toggle {
    width: 100%;
    border: none;
    background: #f3f4f6;
    color: #374151;
    border-radius: 13px;
    padding: 10px;
    font-size: 12px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
    margin-bottom: 10px;
  }

  .details-box {
    display: none;
  }

  .submit-btn {
    width: 100%;
    border: none;
    background: #2563eb;
    color: #ffffff;
    border-radius: 15px;
    padding: 12px;
    font-size: 14px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
  }

  .records-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }

  .records-title strong {
    font-size: 14px;
    font-weight: 900;
    color: #111827;
  }

  .records-title span {
    font-size: 11px;
    color: #6b7280;
    font-weight: 700;
  }

  .empty-records {
    background: #f9fafb;
    color: #6b7280;
    text-align: center;
    border-radius: 14px;
    padding: 16px 10px;
    font-size: 12px;
    line-height: 1.8;
  }

  .record-item {
    border: 1px solid #e5e7eb;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 9px;
    background: #ffffff;
  }

  .record-item:last-child {
    margin-bottom: 0;
  }

  .record-top {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 7px;
  }

  .record-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
  }

  .record-time {
    font-size: 10px;
    color: #9ca3af;
    white-space: nowrap;
  }

  .record-info {
    font-size: 11px;
    color: #4b5563;
    line-height: 1.9;
  }

  .record-total {
    margin-top: 6px;
    font-size: 12px;
    font-weight: 900;
    color: #16a34a;
  }

  .record-desc {
    margin-top: 5px;
    color: #6b7280;
    font-size: 11px;
    line-height: 1.8;
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }

  @media (max-width: 380px) {
    .summary-wrap .summary-card strong {
      font-size: 14px;
    }
  }
</style>

<script>
(function(){
  let selectedService = null;
  let records = [];

  let latestFeedback = {
    type: "positive",
    title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
    description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
    badge: "۸۰٪"
  };

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const services = [
    { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
    { name: "جوشکاری", price: 200000 },
    { name: "نجاری", price: 180000 }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const toast = document.getElementById("toast");

  const serviceSearch = document.getElementById("serviceSearch");
  const serviceResults = document.getElementById("serviceResults");
  const serviceForm = document.getElementById("serviceForm");
  const selectedServiceName = document.getElementById("selectedServiceName");
  const serviceCount = document.getElementById("serviceCount");
  const servicePrice = document.getElementById("servicePrice");
  const serviceDescription = document.getElementById("serviceDescription");
  const submitService = document.getElementById("submitService");
  const todayAmount = document.getElementById("todayAmount");
  const todayCount = document.getElementById("todayCount");
  const recordsList = document.getElementById("recordsList");
  const recordsCountText = document.getElementById("recordsCountText");
  const detailsToggle = document.getElementById("detailsToggle");
  const detailsBox = document.getElementById("detailsBox");
  const bestRecordText = document.getElementById("bestRecordText");
  const recordMessage = document.getElementById("recordMessage");
  const feedbackMain = document.getElementById("feedbackMain");
  const feedbackSub = document.getElementById("feedbackSub");
  const feedbackBadge = document.getElementById("feedbackBadge");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function toPersianNumber(value) {
    return Number(value || 0).toLocaleString("fa-IR");
  }

  function formatToman(value) {
    return toPersianNumber(value) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function showResults(keyword) {
    const text = keyword.trim();
    serviceResults.innerHTML = "";

    if (!text) {
      serviceResults.style.display = "none";
      return;
    }

    const filtered = services.filter(function(service) {
      return service.name.includes(text);
    });

    if (filtered.length === 0) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
        <div class="service-result-price">برای انتخاب این مورد بزنید</div>
      `;
      item.addEventListener("click", function() {
        selectService({ name: text, price: 0 });
      });
      serviceResults.appendChild(item);
      serviceResults.style.display = "block";
      return;
    }

    filtered.forEach(function(service) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">${service.name}</div>
        <div class="service-result-price">${formatToman(service.price)}</div>
      `;
      item.addEventListener("click", function() {
        selectService(service);
      });
      serviceResults.appendChild(item);
    });

    serviceResults.style.display = "block";
  }

  function selectService(service) {
    selectedService = service;
    selectedServiceName.textContent = service.name;
    serviceSearch.value = service.name;
    servicePrice.value = service.price || "";
    serviceCount.value = 1;
    serviceDescription.value = "";
    serviceResults.style.display = "none";
    serviceForm.style.display = "block";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";

    setTimeout(function() {
      serviceCount.focus();
    }, 100);
  }

  function updateSummary() {
    const totalAmount = records.reduce(function(sum, item) {
      return sum + item.total;
    }, 0);

    const totalCount = records.reduce(function(sum, item) {
      return sum + item.count;
    }, 0);

    todayAmount.textContent = formatToman(totalAmount);
    todayCount.textContent = toPersianNumber(totalCount);
  }

  function updatePersonalRecord() {
    bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
    recordMessage.textContent = "";
  }

  function renderRecords() {
    recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

    if (records.length === 0) {
      recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
      return;
    }

    recordsList.innerHTML = "";

    const reversed = records.slice().reverse();
    reversed.forEach(function(item) {
      const div = document.createElement("div");
      div.className = "record-item";
      div.innerHTML = `
        <div class="record-top">
          <div class="record-name">${item.name}</div>
          <div class="record-time">${item.time}</div>
        </div>
        <div class="record-info">
          تعداد: ${toPersianNumber(item.count)} |
          مبلغ واحد: ${formatToman(item.price)}
        </div>
        <div class="record-total">
          جمع: ${formatToman(item.total)}
        </div>
        ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
      `;
      recordsList.appendChild(div);
    });
  }

  function renderFeedback() {
    feedbackMain.textContent = latestFeedback.title;
    feedbackSub.textContent = latestFeedback.description;
    feedbackBadge.textContent = latestFeedback.badge;
    feedbackBadge.className = "feedback-badge " + latestFeedback.type;
  }

  function submitRecord() {
    if (!selectedService) {
      alert("اول یک خدمت را انتخاب کن.");
      return;
    }

    const count = parseInt(serviceCount.value, 10);
    const price = parseInt(servicePrice.value, 10);
    const description = serviceDescription.value.trim();

    if (!count || count <= 0) {
      alert("تعداد را درست وارد کن.");
      return;
    }

    if (isNaN(price) || price < 0) {
      alert("مبلغ را درست وارد کن.");
      return;
    }

    const total = count * price;
    const now = new Date();

    records.push({
      name: selectedService.name,
      count: count,
      price: price,
      total: total,
      description: description,
      time: now.toLocaleTimeString("fa-IR", {
        hour: "2-digit",
        minute: "2-digit"
      })
    });

    renderRecords();
    updateSummary();

    selectedService = null;
    serviceSearch.value = "";
    serviceCount.value = 1;
    servicePrice.value = "";
    serviceDescription.value = "";
    selectedServiceName.textContent = "---";
    serviceForm.style.display = "none";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
    serviceSearch.focus();

    showToast("ثبت جدید اضافه شد");
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmountValue);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderRecords();
    updateSummary();
    updatePersonalRecord();
    renderFeedback();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function() {
    showResults(serviceSearch.value);
  });

  detailsToggle.addEventListener("click", function() {
    if (detailsBox.style.display === "block") {
      detailsBox.style.display = "none";
      detailsToggle.textContent = "افزودن توضیحات اختیاری";
    } else {
      detailsBox.style.display = "block";
      detailsToggle.textContent = "بستن توضیحات";
    }
  });

  submitService.addEventListener("click", submitRecord);

  renderAll();
})();
</script>
محصولات بروز
TEXT - 2026-05-26 19:02:32
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */ if (!defined('ABSPATH')) exit; class PCATC_Settings_Snippet { // Options const OPT_CUTOFF = 'pcatc_cutoff_date'; const OPT_MSG = 'pcatc_message'; const OPT_FALLBACK = 'pcatc_use_modified_fallback'; const OPT_SHOW_FRONT = 'pcatc_show_front_status'; const OPT_SHOW_ADMIN = 'pcatc_show_admin_status'; const OPT_TEXT_FRESH = 'pcatc_text_fresh'; const OPT_TEXT_STALE = 'pcatc_text_stale'; // Meta const META = '_pcatc_price_last_updated'; public function __construct() { // Admin settings UI add_action('admin_menu', [$this, 'add_settings_page']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100); // Stamp when price changes add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); add_action('added_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); // Block add to cart + notices add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5); add_action('woocommerce_before_cart', [$this,'cart_checkout_notice']); add_action('woocommerce_before_checkout_form', [$this,'cart_checkout_notice']); // Front indicators add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11); add_filter('woocommerce_available_variation', [$this,'add_variation_status_data'], 10, 3); add_action('wp_enqueue_scripts', [$this,'enqueue_front_js']); // Admin list indicator add_filter('manage_edit-product_columns', [$this,'add_admin_column'], 30); add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2); add_action('admin_head', [$this,'admin_column_css']); } /* ---------- Defaults ---------- */ private function default_cutoff(): string { return '2026-01-01'; } private function default_msg(): string { return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.'; } private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; } private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; } /* ---------- Options getters ---------- */ private function get_cutoff_date(): string { $val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff()); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff(); return $val; } private function get_msg(): string { $val = (string) get_option(self::OPT_MSG, $this->default_msg()); return $val !== '' ? $val : $this->default_msg(); } private function use_fallback(): bool { return get_option(self::OPT_FALLBACK, 'yes') === 'yes'; } private function show_front(): bool { return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes'; } private function show_admin(): bool { return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes'; } private function text_fresh(): string { $val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh()); return $val !== '' ? $val : $this->default_text_fresh(); } private function text_stale(): string { $val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale()); return $val !== '' ? $val : $this->default_text_stale(); } /* ---------- Cutoff timestamp ---------- */ private function cutoff_ts(): int { $dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } /* ---------- Price update stamp ---------- */ public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void { $type = get_post_type($post_id); if (!in_array($type, ['product','product_variation'], true)) return; if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return; update_post_meta($post_id, self::META, time()); } private function last_update_ts($id): int { $ts = (int) get_post_meta($id, self::META, true); if ($ts > 0) return $ts; if ($this->use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } private function is_stale($id): bool { $ts = $this->last_update_ts($id); if ($ts <= 0) return true; return $ts < $this->cutoff_ts(); } private function status_payload_for($id): array { $stale = $this->is_stale($id); return [ 'is_stale' => $stale ? 1 : 0, 'text' => $stale ? $this->text_stale() : $this->text_fresh(), 'class' => $stale ? 'pcatc-stale' : 'pcatc-fresh', ]; } /* ---------- WooCommerce blocking ---------- */ public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) { $target_id = $variation_id ? (int)$variation_id : (int)$product_id; if ($this->is_stale($target_id)) { wc_add_notice($this->get_msg(), 'error'); return false; } return $passed; } public function cart_checkout_notice(): void { if (!function_exists('WC') || !WC()->cart) return; foreach (WC()->cart->get_cart() as $item) { $target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id']; if ($this->is_stale($target_id)) { wc_print_notice($this->get_msg(), 'error'); break; } } } /* ---------- Front status (simple + variable dynamic) ---------- */ public function render_front_status_block(): void { if (!$this->show_front() || !is_product()) return; global $product; if (!$product instanceof WC_Product) return; // For simple products, render fixed status. // For variable products, we render a container that JS will update on variation selection. $is_variable = $product->is_type('variable'); $payload = $this->status_payload_for($product->get_id()); $text = esc_html($payload['text']); $cls = esc_attr($payload['class']); echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">'; echo $is_variable ? '' : $text; echo '</div>'; } public function add_variation_status_data($variation_data, $product, $variation) { if (!$this->show_front()) return $variation_data; $vid = $variation->get_id(); $p = $this->status_payload_for($vid); $variation_data['pcatc_is_stale'] = $p['is_stale']; $variation_data['pcatc_text'] = $p['text']; $variation_data['pcatc_class'] = $p['class']; return $variation_data; } public function enqueue_front_js(): void { if (!$this->show_front() || !is_product()) return; wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true); wp_enqueue_script('pcatc-front'); // Inline CSS (front) $css = " #pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;} #pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;} "; wp_add_inline_style('woocommerce-inline', $css); // JS: update status when variation changes $js = <<<JS jQuery(function($){ var box = $('#pcatc-price-status'); if(!box.length) return; var form = $('form.variations_form'); if(!form.length) return; // simple product -> no need function setStatus(v){ if(!v || typeof v.pcatc_is_stale === 'undefined'){ // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده) box.text(''); box.removeClass('pcatc-fresh pcatc-stale'); return; } box.text(v.pcatc_text || ''); box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || ''); } form.on('found_variation', function(e, variation){ setStatus(variation); }); form.on('reset_data', function(){ setStatus(null); }); }); JS; wp_add_inline_script('pcatc-front', $js); } /* ---------- Admin list column (green/red dot) ---------- */ public function add_admin_column($columns) { if (!$this->show_admin()) return $columns; // Insert near price column if possible $new = []; foreach ($columns as $key => $label) { $new[$key] = $label; if ($key === 'price') { $new['pcatc_status'] = 'وضعیت قیمت'; } } if (!isset($new['pcatc_status'])) { $new['pcatc_status'] = 'وضعیت قیمت'; } return $new; } public function render_admin_column($column, $post_id) { if (!$this->show_admin()) return; if ($column !== 'pcatc_status') return; // For variable product: if ANY variation is fresh => green else red $product = wc_get_product($post_id); if (!$product) return; $is_fresh = false; if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; } } } } else { $is_fresh = !$this->is_stale($post_id); } echo $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>'; } public function admin_column_css() { if (!$this->show_admin()) return; echo '<style> .column-pcatc_status{width:80px;text-align:center;} .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } /* ---------- Admin settings page ---------- */ public function add_settings_page(): void { add_options_page( 'تنظیمات قفل خرید بر اساس تاریخ', 'قفل خرید (تاریخ قیمت)', 'manage_options', 'pcatc-settings', [$this, 'render_settings_page'] ); } public function register_settings(): void { register_setting('pcatc_settings_group', self::OPT_CUTOFF, [ 'type' => 'string', 'sanitize_callback' => function($v){ $v = trim((string)$v); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff(); } ]); register_setting('pcatc_settings_group', self::OPT_MSG, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_textarea_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_FALLBACK, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); } public function render_settings_page(): void { if (!current_user_can('manage_options')) return; $cutoff = esc_attr($this->get_cutoff_date()); $msg = esc_textarea($this->get_msg()); $fb = $this->use_fallback() ? 'yes' : 'no'; $sf = $this->show_front() ? 'yes' : 'no'; $sa = $this->show_admin() ? 'yes' : 'no'; $tf = esc_attr($this->text_fresh()); $ts = esc_attr($this->text_stale()); ?> <div class="wrap"> <h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1> <form method="post" action="options.php"> <?php settings_fields('pcatc_settings_group'); ?> <table class="form-table" role="presentation"> <tr> <th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th> <td> <input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>"> <p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th> <td> <textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea> <p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p> </td> </tr> <tr> <th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th> <td> <label> <input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>> اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن. </label> </td> </tr> <tr> <th scope="row">نمایش وضعیت قیمت</th> <td> <label style="display:block;margin-bottom:6px;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>> نمایش نقطه سبز/قرمز در لیست محصولات (پنل) </label> <label style="display:block;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>> نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای) </label> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;"> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;"> </td> </tr> </table> <?php submit_button('ذخیره تنظیمات'); ?> </form> </div> <?php } /* ---------- Admin bar shortcut ---------- */ public function admin_bar_link($admin_bar): void { if (!is_admin_bar_showing() || !current_user_can('manage_options')) return; $admin_bar->add_node([ 'id' => 'pcatc_settings_link', 'title' => 'تنظیمات قفل خرید', 'href' => admin_url('options-general.php?page=pcatc-settings'), ]); } } new PCATC_Settings_Snippet(); کد دوم در قسمت زیر /* === Front Dot Indicator on Archives (Shop/Category) === */ if (!defined('ABSPATH')) exit; function pcatc_dot_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01'; $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_dot_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_dot_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_dot_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_dot_is_stale($id) { $ts = pcatc_dot_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_dot_cutoff_ts(); } function pcatc_dot_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_dot_is_stale((int)$vid)) return true; } } return false; } // Simple / others: خود محصول return !pcatc_dot_is_stale((int)$product->get_id()); } /** * Add dot next to price on archives (shop/category/tag) */ function pcatc_dot_price_html($price_html, $product) { if (is_admin()) return $price_html; // فقط صفحات لیست محصولات در سایت if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return $price_html; } // اگر قیمت خالیه، چیزی نزن if (trim(wp_strip_all_tags($price_html)) === '') return $price_html; $is_fresh = pcatc_dot_product_is_fresh($product); $dot = $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>'; // نقطه + فاصله + قیمت return $dot . ' ' . $price_html; } add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2); /** CSS for dots (front) */ function pcatc_dot_css() { if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return; echo '<style> .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } add_action('wp_head', 'pcatc_dot_css', 50);
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */

if (!defined('ABSPATH')) exit;

class PCATC_Settings_Snippet {
	// Options
	const OPT_CUTOFF         = 'pcatc_cutoff_date';
	const OPT_MSG            = 'pcatc_message';
	const OPT_FALLBACK       = 'pcatc_use_modified_fallback';

	const OPT_SHOW_FRONT     = 'pcatc_show_front_status';
	const OPT_SHOW_ADMIN     = 'pcatc_show_admin_status';
	const OPT_TEXT_FRESH     = 'pcatc_text_fresh';
	const OPT_TEXT_STALE     = 'pcatc_text_stale';

	// Meta
	const META               = '_pcatc_price_last_updated';

	public function __construct() {
		// Admin settings UI
		add_action('admin_menu', [$this, 'add_settings_page']);
		add_action('admin_init', [$this, 'register_settings']);
		add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100);

		// Stamp when price changes
		add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4);
		add_action('added_post_meta',   [$this,'maybe_stamp_price_update'], 10, 4);

		// Block add to cart + notices
		add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5);
		add_action('woocommerce_before_cart',            [$this,'cart_checkout_notice']);
		add_action('woocommerce_before_checkout_form',   [$this,'cart_checkout_notice']);

		// Front indicators
		add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11);
		add_filter('woocommerce_available_variation',    [$this,'add_variation_status_data'], 10, 3);
		add_action('wp_enqueue_scripts',                 [$this,'enqueue_front_js']);

		// Admin list indicator
		add_filter('manage_edit-product_columns',        [$this,'add_admin_column'], 30);
		add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2);
		add_action('admin_head',                         [$this,'admin_column_css']);
	}

	/* ---------- Defaults ---------- */
	private function default_cutoff(): string { return '2026-01-01'; }
	private function default_msg(): string {
		return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.';
	}
	private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; }
	private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; }

	/* ---------- Options getters ---------- */
	private function get_cutoff_date(): string {
		$val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff());
		if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff();
		return $val;
	}
	private function get_msg(): string {
		$val = (string) get_option(self::OPT_MSG, $this->default_msg());
		return $val !== '' ? $val : $this->default_msg();
	}
	private function use_fallback(): bool {
		return get_option(self::OPT_FALLBACK, 'yes') === 'yes';
	}
	private function show_front(): bool {
		return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes';
	}
	private function show_admin(): bool {
		return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes';
	}
	private function text_fresh(): string {
		$val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh());
		return $val !== '' ? $val : $this->default_text_fresh();
	}
	private function text_stale(): string {
		$val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale());
		return $val !== '' ? $val : $this->default_text_stale();
	}

	/* ---------- Cutoff timestamp ---------- */
	private function cutoff_ts(): int {
		$dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone());
		return $dt->getTimestamp();
	}

	/* ---------- Price update stamp ---------- */
	public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void {
		$type = get_post_type($post_id);
		if (!in_array($type, ['product','product_variation'], true)) return;

		if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return;

		update_post_meta($post_id, self::META, time());
	}

	private function last_update_ts($id): int {
		$ts = (int) get_post_meta($id, self::META, true);
		if ($ts > 0) return $ts;

		if ($this->use_fallback()) {
			$post = get_post($id);
			if ($post && !empty($post->post_modified_gmt)) {
				$t = strtotime($post->post_modified_gmt . ' GMT');
				if ($t) return $t;
			}
		}
		return 0;
	}

	private function is_stale($id): bool {
		$ts = $this->last_update_ts($id);
		if ($ts <= 0) return true;
		return $ts < $this->cutoff_ts();
	}

	private function status_payload_for($id): array {
		$stale = $this->is_stale($id);
		return [
			'is_stale' => $stale ? 1 : 0,
			'text'     => $stale ? $this->text_stale() : $this->text_fresh(),
			'class'    => $stale ? 'pcatc-stale' : 'pcatc-fresh',
		];
	}

	/* ---------- WooCommerce blocking ---------- */
	public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) {
		$target_id = $variation_id ? (int)$variation_id : (int)$product_id;

		if ($this->is_stale($target_id)) {
			wc_add_notice($this->get_msg(), 'error');
			return false;
		}
		return $passed;
	}

	public function cart_checkout_notice(): void {
		if (!function_exists('WC') || !WC()->cart) return;

		foreach (WC()->cart->get_cart() as $item) {
			$target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id'];
			if ($this->is_stale($target_id)) {
				wc_print_notice($this->get_msg(), 'error');
				break;
			}
		}
	}

	/* ---------- Front status (simple + variable dynamic) ---------- */
	public function render_front_status_block(): void {
		if (!$this->show_front() || !is_product()) return;

		global $product;
		if (!$product instanceof WC_Product) return;

		// For simple products, render fixed status.
		// For variable products, we render a container that JS will update on variation selection.
		$is_variable = $product->is_type('variable');

		$payload = $this->status_payload_for($product->get_id());
		$text = esc_html($payload['text']);
		$cls  = esc_attr($payload['class']);

		echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">';
		echo $is_variable ? '' : $text;
		echo '</div>';
	}

	public function add_variation_status_data($variation_data, $product, $variation) {
		if (!$this->show_front()) return $variation_data;

		$vid = $variation->get_id();
		$p = $this->status_payload_for($vid);

		$variation_data['pcatc_is_stale'] = $p['is_stale'];
		$variation_data['pcatc_text']     = $p['text'];
		$variation_data['pcatc_class']    = $p['class'];

		return $variation_data;
	}

	public function enqueue_front_js(): void {
		if (!$this->show_front() || !is_product()) return;

		wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true);
		wp_enqueue_script('pcatc-front');

		// Inline CSS (front)
		$css = "
#pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;}
#pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;}
";
		wp_add_inline_style('woocommerce-inline', $css);

		// JS: update status when variation changes
		$js = <<<JS
jQuery(function($){
  var box = $('#pcatc-price-status');
  if(!box.length) return;

  var form = $('form.variations_form');
  if(!form.length) return; // simple product -> no need

  function setStatus(v){
    if(!v || typeof v.pcatc_is_stale === 'undefined'){
      // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده)
      box.text('');
      box.removeClass('pcatc-fresh pcatc-stale');
      return;
    }
    box.text(v.pcatc_text || '');
    box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || '');
  }

  form.on('found_variation', function(e, variation){
    setStatus(variation);
  });

  form.on('reset_data', function(){
    setStatus(null);
  });
});
JS;
		wp_add_inline_script('pcatc-front', $js);
	}

	/* ---------- Admin list column (green/red dot) ---------- */
	public function add_admin_column($columns) {
		if (!$this->show_admin()) return $columns;

		// Insert near price column if possible
		$new = [];
		foreach ($columns as $key => $label) {
			$new[$key] = $label;
			if ($key === 'price') {
				$new['pcatc_status'] = 'وضعیت قیمت';
			}
		}
		if (!isset($new['pcatc_status'])) {
			$new['pcatc_status'] = 'وضعیت قیمت';
		}
		return $new;
	}

	public function render_admin_column($column, $post_id) {
		if (!$this->show_admin()) return;
		if ($column !== 'pcatc_status') return;

		// For variable product: if ANY variation is fresh => green else red
		$product = wc_get_product($post_id);
		if (!$product) return;

		$is_fresh = false;

		if ($product->is_type('variable')) {
			$children = $product->get_children();
			if (!empty($children)) {
				foreach ($children as $vid) {
					if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; }
				}
			}
		} else {
			$is_fresh = !$this->is_stale($post_id);
		}

		echo $is_fresh
			? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>'
			: '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>';
	}

	public function admin_column_css() {
		if (!$this->show_admin()) return;
		echo '<style>
			.column-pcatc_status{width:80px;text-align:center;}
			.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;}
			.pcatc-dot-green{background:#19a64a;}
			.pcatc-dot-red{background:#d10000;}
		</style>';
	}

	/* ---------- Admin settings page ---------- */
	public function add_settings_page(): void {
		add_options_page(
			'تنظیمات قفل خرید بر اساس تاریخ',
			'قفل خرید (تاریخ قیمت)',
			'manage_options',
			'pcatc-settings',
			[$this, 'render_settings_page']
		);
	}

	public function register_settings(): void {
		register_setting('pcatc_settings_group', self::OPT_CUTOFF, [
			'type' => 'string',
			'sanitize_callback' => function($v){
				$v = trim((string)$v);
				return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff();
			}
		]);

		register_setting('pcatc_settings_group', self::OPT_MSG, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_textarea_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_FALLBACK, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);
	}

	public function render_settings_page(): void {
		if (!current_user_can('manage_options')) return;

		$cutoff = esc_attr($this->get_cutoff_date());
		$msg    = esc_textarea($this->get_msg());
		$fb     = $this->use_fallback() ? 'yes' : 'no';

		$sf     = $this->show_front() ? 'yes' : 'no';
		$sa     = $this->show_admin() ? 'yes' : 'no';

		$tf     = esc_attr($this->text_fresh());
		$ts     = esc_attr($this->text_stale());
		?>
		<div class="wrap">
			<h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1>
			<form method="post" action="options.php">
				<?php settings_fields('pcatc_settings_group'); ?>

				<table class="form-table" role="presentation">
					<tr>
						<th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th>
						<td>
							<input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>">
							<p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th>
						<td>
							<textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea>
							<p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th>
						<td>
							<label>
								<input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>>
								اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن.
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row">نمایش وضعیت قیمت</th>
						<td>
							<label style="display:block;margin-bottom:6px;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>>
								نمایش نقطه سبز/قرمز در لیست محصولات (پنل)
							</label>

							<label style="display:block;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>>
								نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای)
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;">
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;">
						</td>
					</tr>
				</table>

				<?php submit_button('ذخیره تنظیمات'); ?>
			</form>
		</div>
		<?php
	}

	/* ---------- Admin bar shortcut ---------- */
	public function admin_bar_link($admin_bar): void {
		if (!is_admin_bar_showing() || !current_user_can('manage_options')) return;
		$admin_bar->add_node([
			'id'    => 'pcatc_settings_link',
			'title' => 'تنظیمات قفل خرید',
			'href'  => admin_url('options-general.php?page=pcatc-settings'),
		]);
	}
}

new PCATC_Settings_Snippet();

کد دوم در قسمت زیر


/* === Front Dot Indicator on Archives (Shop/Category) === */

if (!defined('ABSPATH')) exit;

function pcatc_dot_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01';
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_dot_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_dot_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_dot_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_dot_is_stale($id) {
	$ts = pcatc_dot_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_dot_cutoff_ts();
}

function pcatc_dot_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_dot_is_stale((int)$vid)) return true;
			}
		}
		return false;
	}

	// Simple / others: خود محصول
	return !pcatc_dot_is_stale((int)$product->get_id());
}

/**
 * Add dot next to price on archives (shop/category/tag)
 */
function pcatc_dot_price_html($price_html, $product) {
	if (is_admin()) return $price_html;

	// فقط صفحات لیست محصولات در سایت
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return $price_html;
	}

	// اگر قیمت خالیه، چیزی نزن
	if (trim(wp_strip_all_tags($price_html)) === '') return $price_html;

	$is_fresh = pcatc_dot_product_is_fresh($product);
	$dot = $is_fresh
		? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>'
		: '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>';

	// نقطه + فاصله + قیمت
	return $dot . ' ' . $price_html;
}
add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2);

/** CSS for dots (front) */
function pcatc_dot_css() {
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return;

	echo '<style>
	.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);}
	.pcatc-dot-green{background:#19a64a;}
	.pcatc-dot-red{background:#d10000;}
	</style>';
}
add_action('wp_head', 'pcatc_dot_css', 50);


۸۸
TEXT - 2026-05-26 19:02:26
add_shortcode('pcatc_product_lists', function() { if (!current_user_can('manage_woocommerce') && !current_user_can('manage_options')) { return 'دسترسی ندارید.'; } if (!function_exists('wc_get_product')) { return 'ووکامرس فعال نیست.'; } $product_ids = get_posts([ 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'orderby' => 'title', 'order' => 'ASC', ]); $fresh = []; $stale = []; foreach ($product_ids as $product_id) { $product = wc_get_product($product_id); if (!$product) continue; if (function_exists('pcatc_dot_product_is_fresh')) { $is_fresh = pcatc_dot_product_is_fresh($product); } elseif (function_exists('pcatc_filter3_product_is_fresh')) { $is_fresh = pcatc_filter3_product_is_fresh($product); } elseif (function_exists('pcatc_filter2_product_is_fresh')) { $is_fresh = pcatc_filter2_product_is_fresh($product); } else { return 'تابع تشخیص سبز/قرمز پیدا نشد.'; } $item = [ 'id' => $product_id, 'title' => get_the_title($product_id), 'url' => get_permalink($product_id), ]; if ($is_fresh) { $fresh[] = $item; } else { $stale[] = $item; } } ob_start(); ?> <div style="direction:rtl;text-align:right;font-family:tahoma,arial;line-height:2"> <h2 style="color:green">محصولات سبز / به‌روز</h2> <p>تعداد: <?php echo count($fresh); ?></p> <ol> <?php foreach ($fresh as $item): ?> <li> <a href="<?php echo esc_url($item['url']); ?>" target="_blank"> <?php echo esc_html($item['title']); ?> </a> <small style="color:#777">ID: <?php echo (int) $item['id']; ?></small> </li> <?php endforeach; ?> </ol> <hr> <h2 style="color:red">محصولات قرمز / قدیمی</h2> <p>تعداد: <?php echo count($stale); ?></p> <ol> <?php foreach ($stale as $item): ?> <li> <a href="<?php echo esc_url($item['url']); ?>" target="_blank"> <?php echo esc_html($item['title']); ?> </a> <small style="color:#777">ID: <?php echo (int) $item['id']; ?></small> </li> <?php endforeach; ?> </ol> </div> <?php return ob_get_clean(); });
add_shortcode('pcatc_product_lists', function() {
	if (!current_user_can('manage_woocommerce') && !current_user_can('manage_options')) {
		return 'دسترسی ندارید.';
	}

	if (!function_exists('wc_get_product')) {
		return 'ووکامرس فعال نیست.';
	}

	$product_ids = get_posts([
		'post_type'      => 'product',
		'post_status'    => 'publish',
		'posts_per_page' => -1,
		'fields'         => 'ids',
		'orderby'        => 'title',
		'order'          => 'ASC',
	]);

	$fresh = [];
	$stale = [];

	foreach ($product_ids as $product_id) {
		$product = wc_get_product($product_id);
		if (!$product) continue;

		if (function_exists('pcatc_dot_product_is_fresh')) {
			$is_fresh = pcatc_dot_product_is_fresh($product);
		} elseif (function_exists('pcatc_filter3_product_is_fresh')) {
			$is_fresh = pcatc_filter3_product_is_fresh($product);
		} elseif (function_exists('pcatc_filter2_product_is_fresh')) {
			$is_fresh = pcatc_filter2_product_is_fresh($product);
		} else {
			return 'تابع تشخیص سبز/قرمز پیدا نشد.';
		}

		$item = [
			'id'    => $product_id,
			'title' => get_the_title($product_id),
			'url'   => get_permalink($product_id),
		];

		if ($is_fresh) {
			$fresh[] = $item;
		} else {
			$stale[] = $item;
		}
	}

	ob_start();
	?>

	<div style="direction:rtl;text-align:right;font-family:tahoma,arial;line-height:2">

		<h2 style="color:green">محصولات سبز / به‌روز</h2>
		<p>تعداد: <?php echo count($fresh); ?></p>

		<ol>
			<?php foreach ($fresh as $item): ?>
				<li>
					<a href="<?php echo esc_url($item['url']); ?>" target="_blank">
						<?php echo esc_html($item['title']); ?>
					</a>
					<small style="color:#777">ID: <?php echo (int) $item['id']; ?></small>
				</li>
			<?php endforeach; ?>
		</ol>

		<hr>

		<h2 style="color:red">محصولات قرمز / قدیمی</h2>
		<p>تعداد: <?php echo count($stale); ?></p>

		<ol>
			<?php foreach ($stale as $item): ?>
				<li>
					<a href="<?php echo esc_url($item['url']); ?>" target="_blank">
						<?php echo esc_html($item['title']); ?>
					</a>
					<small style="color:#777">ID: <?php echo (int) $item['id']; ?></small>
				</li>
			<?php endforeach; ?>
		</ol>

	</div>

	<?php
	return ob_get_clean();
});
محصولات بروز
TEXT - 2026-05-26 18:54:30
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */ if (!defined('ABSPATH')) exit; class PCATC_Settings_Snippet { // Options const OPT_CUTOFF = 'pcatc_cutoff_date'; const OPT_MSG = 'pcatc_message'; const OPT_FALLBACK = 'pcatc_use_modified_fallback'; const OPT_SHOW_FRONT = 'pcatc_show_front_status'; const OPT_SHOW_ADMIN = 'pcatc_show_admin_status'; const OPT_TEXT_FRESH = 'pcatc_text_fresh'; const OPT_TEXT_STALE = 'pcatc_text_stale'; // Meta const META = '_pcatc_price_last_updated'; public function __construct() { // Admin settings UI add_action('admin_menu', [$this, 'add_settings_page']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100); // Stamp when price changes add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); add_action('added_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); // Block add to cart + notices add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5); add_action('woocommerce_before_cart', [$this,'cart_checkout_notice']); add_action('woocommerce_before_checkout_form', [$this,'cart_checkout_notice']); // Front indicators add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11); add_filter('woocommerce_available_variation', [$this,'add_variation_status_data'], 10, 3); add_action('wp_enqueue_scripts', [$this,'enqueue_front_js']); // Admin list indicator add_filter('manage_edit-product_columns', [$this,'add_admin_column'], 30); add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2); add_action('admin_head', [$this,'admin_column_css']); } /* ---------- Defaults ---------- */ private function default_cutoff(): string { return '2026-01-01'; } private function default_msg(): string { return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.'; } private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; } private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; } /* ---------- Options getters ---------- */ private function get_cutoff_date(): string { $val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff()); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff(); return $val; } private function get_msg(): string { $val = (string) get_option(self::OPT_MSG, $this->default_msg()); return $val !== '' ? $val : $this->default_msg(); } private function use_fallback(): bool { return get_option(self::OPT_FALLBACK, 'yes') === 'yes'; } private function show_front(): bool { return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes'; } private function show_admin(): bool { return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes'; } private function text_fresh(): string { $val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh()); return $val !== '' ? $val : $this->default_text_fresh(); } private function text_stale(): string { $val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale()); return $val !== '' ? $val : $this->default_text_stale(); } /* ---------- Cutoff timestamp ---------- */ private function cutoff_ts(): int { $dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } /* ---------- Price update stamp ---------- */ public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void { $type = get_post_type($post_id); if (!in_array($type, ['product','product_variation'], true)) return; if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return; update_post_meta($post_id, self::META, time()); } private function last_update_ts($id): int { $ts = (int) get_post_meta($id, self::META, true); if ($ts > 0) return $ts; if ($this->use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } private function is_stale($id): bool { $ts = $this->last_update_ts($id); if ($ts <= 0) return true; return $ts < $this->cutoff_ts(); } private function status_payload_for($id): array { $stale = $this->is_stale($id); return [ 'is_stale' => $stale ? 1 : 0, 'text' => $stale ? $this->text_stale() : $this->text_fresh(), 'class' => $stale ? 'pcatc-stale' : 'pcatc-fresh', ]; } /* ---------- WooCommerce blocking ---------- */ public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) { $target_id = $variation_id ? (int)$variation_id : (int)$product_id; if ($this->is_stale($target_id)) { wc_add_notice($this->get_msg(), 'error'); return false; } return $passed; } public function cart_checkout_notice(): void { if (!function_exists('WC') || !WC()->cart) return; foreach (WC()->cart->get_cart() as $item) { $target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id']; if ($this->is_stale($target_id)) { wc_print_notice($this->get_msg(), 'error'); break; } } } /* ---------- Front status (simple + variable dynamic) ---------- */ public function render_front_status_block(): void { if (!$this->show_front() || !is_product()) return; global $product; if (!$product instanceof WC_Product) return; // For simple products, render fixed status. // For variable products, we render a container that JS will update on variation selection. $is_variable = $product->is_type('variable'); $payload = $this->status_payload_for($product->get_id()); $text = esc_html($payload['text']); $cls = esc_attr($payload['class']); echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">'; echo $is_variable ? '' : $text; echo '</div>'; } public function add_variation_status_data($variation_data, $product, $variation) { if (!$this->show_front()) return $variation_data; $vid = $variation->get_id(); $p = $this->status_payload_for($vid); $variation_data['pcatc_is_stale'] = $p['is_stale']; $variation_data['pcatc_text'] = $p['text']; $variation_data['pcatc_class'] = $p['class']; return $variation_data; } public function enqueue_front_js(): void { if (!$this->show_front() || !is_product()) return; wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true); wp_enqueue_script('pcatc-front'); // Inline CSS (front) $css = " #pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;} #pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;} "; wp_add_inline_style('woocommerce-inline', $css); // JS: update status when variation changes $js = <<<JS jQuery(function($){ var box = $('#pcatc-price-status'); if(!box.length) return; var form = $('form.variations_form'); if(!form.length) return; // simple product -> no need function setStatus(v){ if(!v || typeof v.pcatc_is_stale === 'undefined'){ // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده) box.text(''); box.removeClass('pcatc-fresh pcatc-stale'); return; } box.text(v.pcatc_text || ''); box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || ''); } form.on('found_variation', function(e, variation){ setStatus(variation); }); form.on('reset_data', function(){ setStatus(null); }); }); JS; wp_add_inline_script('pcatc-front', $js); } /* ---------- Admin list column (green/red dot) ---------- */ public function add_admin_column($columns) { if (!$this->show_admin()) return $columns; // Insert near price column if possible $new = []; foreach ($columns as $key => $label) { $new[$key] = $label; if ($key === 'price') { $new['pcatc_status'] = 'وضعیت قیمت'; } } if (!isset($new['pcatc_status'])) { $new['pcatc_status'] = 'وضعیت قیمت'; } return $new; } public function render_admin_column($column, $post_id) { if (!$this->show_admin()) return; if ($column !== 'pcatc_status') return; // For variable product: if ANY variation is fresh => green else red $product = wc_get_product($post_id); if (!$product) return; $is_fresh = false; if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; } } } } else { $is_fresh = !$this->is_stale($post_id); } echo $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>'; } public function admin_column_css() { if (!$this->show_admin()) return; echo '<style> .column-pcatc_status{width:80px;text-align:center;} .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } /* ---------- Admin settings page ---------- */ public function add_settings_page(): void { add_options_page( 'تنظیمات قفل خرید بر اساس تاریخ', 'قفل خرید (تاریخ قیمت)', 'manage_options', 'pcatc-settings', [$this, 'render_settings_page'] ); } public function register_settings(): void { register_setting('pcatc_settings_group', self::OPT_CUTOFF, [ 'type' => 'string', 'sanitize_callback' => function($v){ $v = trim((string)$v); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff(); } ]); register_setting('pcatc_settings_group', self::OPT_MSG, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_textarea_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_FALLBACK, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); } public function render_settings_page(): void { if (!current_user_can('manage_options')) return; $cutoff = esc_attr($this->get_cutoff_date()); $msg = esc_textarea($this->get_msg()); $fb = $this->use_fallback() ? 'yes' : 'no'; $sf = $this->show_front() ? 'yes' : 'no'; $sa = $this->show_admin() ? 'yes' : 'no'; $tf = esc_attr($this->text_fresh()); $ts = esc_attr($this->text_stale()); ?> <div class="wrap"> <h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1> <form method="post" action="options.php"> <?php settings_fields('pcatc_settings_group'); ?> <table class="form-table" role="presentation"> <tr> <th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th> <td> <input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>"> <p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th> <td> <textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea> <p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p> </td> </tr> <tr> <th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th> <td> <label> <input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>> اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن. </label> </td> </tr> <tr> <th scope="row">نمایش وضعیت قیمت</th> <td> <label style="display:block;margin-bottom:6px;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>> نمایش نقطه سبز/قرمز در لیست محصولات (پنل) </label> <label style="display:block;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>> نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای) </label> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;"> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;"> </td> </tr> </table> <?php submit_button('ذخیره تنظیمات'); ?> </form> </div> <?php } /* ---------- Admin bar shortcut ---------- */ public function admin_bar_link($admin_bar): void { if (!is_admin_bar_showing() || !current_user_can('manage_options')) return; $admin_bar->add_node([ 'id' => 'pcatc_settings_link', 'title' => 'تنظیمات قفل خرید', 'href' => admin_url('options-general.php?page=pcatc-settings'), ]); } } new PCATC_Settings_Snippet(); کد دوم در قسمت زیر /* === Front Dot Indicator on Archives (Shop/Category) === */ if (!defined('ABSPATH')) exit; function pcatc_dot_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01'; $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_dot_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_dot_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_dot_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_dot_is_stale($id) { $ts = pcatc_dot_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_dot_cutoff_ts(); } function pcatc_dot_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_dot_is_stale((int)$vid)) return true; } } return false; } // Simple / others: خود محصول return !pcatc_dot_is_stale((int)$product->get_id()); } /** * Add dot next to price on archives (shop/category/tag) */ function pcatc_dot_price_html($price_html, $product) { if (is_admin()) return $price_html; // فقط صفحات لیست محصولات در سایت if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return $price_html; } // اگر قیمت خالیه، چیزی نزن if (trim(wp_strip_all_tags($price_html)) === '') return $price_html; $is_fresh = pcatc_dot_product_is_fresh($product); $dot = $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>'; // نقطه + فاصله + قیمت return $dot . ' ' . $price_html; } add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2); /** CSS for dots (front) */ function pcatc_dot_css() { if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return; echo '<style> .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } add_action('wp_head', 'pcatc_dot_css', 50);
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */

if (!defined('ABSPATH')) exit;

class PCATC_Settings_Snippet {
	// Options
	const OPT_CUTOFF         = 'pcatc_cutoff_date';
	const OPT_MSG            = 'pcatc_message';
	const OPT_FALLBACK       = 'pcatc_use_modified_fallback';

	const OPT_SHOW_FRONT     = 'pcatc_show_front_status';
	const OPT_SHOW_ADMIN     = 'pcatc_show_admin_status';
	const OPT_TEXT_FRESH     = 'pcatc_text_fresh';
	const OPT_TEXT_STALE     = 'pcatc_text_stale';

	// Meta
	const META               = '_pcatc_price_last_updated';

	public function __construct() {
		// Admin settings UI
		add_action('admin_menu', [$this, 'add_settings_page']);
		add_action('admin_init', [$this, 'register_settings']);
		add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100);

		// Stamp when price changes
		add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4);
		add_action('added_post_meta',   [$this,'maybe_stamp_price_update'], 10, 4);

		// Block add to cart + notices
		add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5);
		add_action('woocommerce_before_cart',            [$this,'cart_checkout_notice']);
		add_action('woocommerce_before_checkout_form',   [$this,'cart_checkout_notice']);

		// Front indicators
		add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11);
		add_filter('woocommerce_available_variation',    [$this,'add_variation_status_data'], 10, 3);
		add_action('wp_enqueue_scripts',                 [$this,'enqueue_front_js']);

		// Admin list indicator
		add_filter('manage_edit-product_columns',        [$this,'add_admin_column'], 30);
		add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2);
		add_action('admin_head',                         [$this,'admin_column_css']);
	}

	/* ---------- Defaults ---------- */
	private function default_cutoff(): string { return '2026-01-01'; }
	private function default_msg(): string {
		return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.';
	}
	private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; }
	private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; }

	/* ---------- Options getters ---------- */
	private function get_cutoff_date(): string {
		$val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff());
		if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff();
		return $val;
	}
	private function get_msg(): string {
		$val = (string) get_option(self::OPT_MSG, $this->default_msg());
		return $val !== '' ? $val : $this->default_msg();
	}
	private function use_fallback(): bool {
		return get_option(self::OPT_FALLBACK, 'yes') === 'yes';
	}
	private function show_front(): bool {
		return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes';
	}
	private function show_admin(): bool {
		return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes';
	}
	private function text_fresh(): string {
		$val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh());
		return $val !== '' ? $val : $this->default_text_fresh();
	}
	private function text_stale(): string {
		$val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale());
		return $val !== '' ? $val : $this->default_text_stale();
	}

	/* ---------- Cutoff timestamp ---------- */
	private function cutoff_ts(): int {
		$dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone());
		return $dt->getTimestamp();
	}

	/* ---------- Price update stamp ---------- */
	public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void {
		$type = get_post_type($post_id);
		if (!in_array($type, ['product','product_variation'], true)) return;

		if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return;

		update_post_meta($post_id, self::META, time());
	}

	private function last_update_ts($id): int {
		$ts = (int) get_post_meta($id, self::META, true);
		if ($ts > 0) return $ts;

		if ($this->use_fallback()) {
			$post = get_post($id);
			if ($post && !empty($post->post_modified_gmt)) {
				$t = strtotime($post->post_modified_gmt . ' GMT');
				if ($t) return $t;
			}
		}
		return 0;
	}

	private function is_stale($id): bool {
		$ts = $this->last_update_ts($id);
		if ($ts <= 0) return true;
		return $ts < $this->cutoff_ts();
	}

	private function status_payload_for($id): array {
		$stale = $this->is_stale($id);
		return [
			'is_stale' => $stale ? 1 : 0,
			'text'     => $stale ? $this->text_stale() : $this->text_fresh(),
			'class'    => $stale ? 'pcatc-stale' : 'pcatc-fresh',
		];
	}

	/* ---------- WooCommerce blocking ---------- */
	public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) {
		$target_id = $variation_id ? (int)$variation_id : (int)$product_id;

		if ($this->is_stale($target_id)) {
			wc_add_notice($this->get_msg(), 'error');
			return false;
		}
		return $passed;
	}

	public function cart_checkout_notice(): void {
		if (!function_exists('WC') || !WC()->cart) return;

		foreach (WC()->cart->get_cart() as $item) {
			$target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id'];
			if ($this->is_stale($target_id)) {
				wc_print_notice($this->get_msg(), 'error');
				break;
			}
		}
	}

	/* ---------- Front status (simple + variable dynamic) ---------- */
	public function render_front_status_block(): void {
		if (!$this->show_front() || !is_product()) return;

		global $product;
		if (!$product instanceof WC_Product) return;

		// For simple products, render fixed status.
		// For variable products, we render a container that JS will update on variation selection.
		$is_variable = $product->is_type('variable');

		$payload = $this->status_payload_for($product->get_id());
		$text = esc_html($payload['text']);
		$cls  = esc_attr($payload['class']);

		echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">';
		echo $is_variable ? '' : $text;
		echo '</div>';
	}

	public function add_variation_status_data($variation_data, $product, $variation) {
		if (!$this->show_front()) return $variation_data;

		$vid = $variation->get_id();
		$p = $this->status_payload_for($vid);

		$variation_data['pcatc_is_stale'] = $p['is_stale'];
		$variation_data['pcatc_text']     = $p['text'];
		$variation_data['pcatc_class']    = $p['class'];

		return $variation_data;
	}

	public function enqueue_front_js(): void {
		if (!$this->show_front() || !is_product()) return;

		wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true);
		wp_enqueue_script('pcatc-front');

		// Inline CSS (front)
		$css = "
#pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;}
#pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;}
";
		wp_add_inline_style('woocommerce-inline', $css);

		// JS: update status when variation changes
		$js = <<<JS
jQuery(function($){
  var box = $('#pcatc-price-status');
  if(!box.length) return;

  var form = $('form.variations_form');
  if(!form.length) return; // simple product -> no need

  function setStatus(v){
    if(!v || typeof v.pcatc_is_stale === 'undefined'){
      // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده)
      box.text('');
      box.removeClass('pcatc-fresh pcatc-stale');
      return;
    }
    box.text(v.pcatc_text || '');
    box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || '');
  }

  form.on('found_variation', function(e, variation){
    setStatus(variation);
  });

  form.on('reset_data', function(){
    setStatus(null);
  });
});
JS;
		wp_add_inline_script('pcatc-front', $js);
	}

	/* ---------- Admin list column (green/red dot) ---------- */
	public function add_admin_column($columns) {
		if (!$this->show_admin()) return $columns;

		// Insert near price column if possible
		$new = [];
		foreach ($columns as $key => $label) {
			$new[$key] = $label;
			if ($key === 'price') {
				$new['pcatc_status'] = 'وضعیت قیمت';
			}
		}
		if (!isset($new['pcatc_status'])) {
			$new['pcatc_status'] = 'وضعیت قیمت';
		}
		return $new;
	}

	public function render_admin_column($column, $post_id) {
		if (!$this->show_admin()) return;
		if ($column !== 'pcatc_status') return;

		// For variable product: if ANY variation is fresh => green else red
		$product = wc_get_product($post_id);
		if (!$product) return;

		$is_fresh = false;

		if ($product->is_type('variable')) {
			$children = $product->get_children();
			if (!empty($children)) {
				foreach ($children as $vid) {
					if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; }
				}
			}
		} else {
			$is_fresh = !$this->is_stale($post_id);
		}

		echo $is_fresh
			? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>'
			: '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>';
	}

	public function admin_column_css() {
		if (!$this->show_admin()) return;
		echo '<style>
			.column-pcatc_status{width:80px;text-align:center;}
			.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;}
			.pcatc-dot-green{background:#19a64a;}
			.pcatc-dot-red{background:#d10000;}
		</style>';
	}

	/* ---------- Admin settings page ---------- */
	public function add_settings_page(): void {
		add_options_page(
			'تنظیمات قفل خرید بر اساس تاریخ',
			'قفل خرید (تاریخ قیمت)',
			'manage_options',
			'pcatc-settings',
			[$this, 'render_settings_page']
		);
	}

	public function register_settings(): void {
		register_setting('pcatc_settings_group', self::OPT_CUTOFF, [
			'type' => 'string',
			'sanitize_callback' => function($v){
				$v = trim((string)$v);
				return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff();
			}
		]);

		register_setting('pcatc_settings_group', self::OPT_MSG, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_textarea_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_FALLBACK, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);
	}

	public function render_settings_page(): void {
		if (!current_user_can('manage_options')) return;

		$cutoff = esc_attr($this->get_cutoff_date());
		$msg    = esc_textarea($this->get_msg());
		$fb     = $this->use_fallback() ? 'yes' : 'no';

		$sf     = $this->show_front() ? 'yes' : 'no';
		$sa     = $this->show_admin() ? 'yes' : 'no';

		$tf     = esc_attr($this->text_fresh());
		$ts     = esc_attr($this->text_stale());
		?>
		<div class="wrap">
			<h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1>
			<form method="post" action="options.php">
				<?php settings_fields('pcatc_settings_group'); ?>

				<table class="form-table" role="presentation">
					<tr>
						<th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th>
						<td>
							<input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>">
							<p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th>
						<td>
							<textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea>
							<p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th>
						<td>
							<label>
								<input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>>
								اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن.
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row">نمایش وضعیت قیمت</th>
						<td>
							<label style="display:block;margin-bottom:6px;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>>
								نمایش نقطه سبز/قرمز در لیست محصولات (پنل)
							</label>

							<label style="display:block;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>>
								نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای)
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;">
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;">
						</td>
					</tr>
				</table>

				<?php submit_button('ذخیره تنظیمات'); ?>
			</form>
		</div>
		<?php
	}

	/* ---------- Admin bar shortcut ---------- */
	public function admin_bar_link($admin_bar): void {
		if (!is_admin_bar_showing() || !current_user_can('manage_options')) return;
		$admin_bar->add_node([
			'id'    => 'pcatc_settings_link',
			'title' => 'تنظیمات قفل خرید',
			'href'  => admin_url('options-general.php?page=pcatc-settings'),
		]);
	}
}

new PCATC_Settings_Snippet();

کد دوم در قسمت زیر


/* === Front Dot Indicator on Archives (Shop/Category) === */

if (!defined('ABSPATH')) exit;

function pcatc_dot_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01';
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_dot_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_dot_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_dot_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_dot_is_stale($id) {
	$ts = pcatc_dot_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_dot_cutoff_ts();
}

function pcatc_dot_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_dot_is_stale((int)$vid)) return true;
			}
		}
		return false;
	}

	// Simple / others: خود محصول
	return !pcatc_dot_is_stale((int)$product->get_id());
}

/**
 * Add dot next to price on archives (shop/category/tag)
 */
function pcatc_dot_price_html($price_html, $product) {
	if (is_admin()) return $price_html;

	// فقط صفحات لیست محصولات در سایت
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return $price_html;
	}

	// اگر قیمت خالیه، چیزی نزن
	if (trim(wp_strip_all_tags($price_html)) === '') return $price_html;

	$is_fresh = pcatc_dot_product_is_fresh($product);
	$dot = $is_fresh
		? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>'
		: '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>';

	// نقطه + فاصله + قیمت
	return $dot . ' ' . $price_html;
}
add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2);

/** CSS for dots (front) */
function pcatc_dot_css() {
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return;

	echo '<style>
	.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);}
	.pcatc-dot-green{background:#19a64a;}
	.pcatc-dot-red{background:#d10000;}
	</style>';
}
add_action('wp_head', 'pcatc_dot_css', 50);


تی تی
TEXT - 2026-05-26 18:54:21
if (!defined('ABSPATH')) exit; /* ========= SAME LOGIC FOR ARCHIVE FILTER ========= */ function pcatc_filter3_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) { $cutoff = '2026-01-01'; } $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_filter3_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_filter3_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_filter3_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_filter3_is_stale($id) { $ts = pcatc_filter3_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_filter3_cutoff_ts(); } function pcatc_filter3_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_filter3_is_stale((int) $vid)) { return true; } } } return false; } return !pcatc_filter3_is_stale((int) $product->get_id()); } function pcatc_filter3_get_matching_product_ids($status = 'fresh') { $args = [ 'post_type' => 'product', 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1, 'no_found_rows' => true, 'tax_query' => WC()->query ? WC()->query->get_tax_query() : [], 'meta_query' => WC()->query ? WC()->query->get_meta_query() : [], ]; $product_ids = get_posts($args); if (empty($product_ids)) return [0]; $matched = []; foreach ($product_ids as $product_id) { $product = wc_get_product($product_id); if (!$product) continue; $is_fresh = pcatc_filter3_product_is_fresh($product); if ($status === 'fresh' && $is_fresh) { $matched[] = $product_id; } elseif ($status === 'stale' && !$is_fresh) { $matched[] = $product_id; } } return !empty($matched) ? $matched : [0]; } add_action('woocommerce_product_query', function($q) { if (is_admin()) return; $status = isset($_GET['pcatc_status']) ? sanitize_text_field(wp_unslash($_GET['pcatc_status'])) : ''; if (!in_array($status, ['fresh', 'stale'], true)) return; if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return; } $matched_ids = pcatc_filter3_get_matching_product_ids($status); $q->set('post__in', $matched_ids); $q->set('orderby', 'post__in'); }, 999);
if (!defined('ABSPATH')) exit;

/* ========= SAME LOGIC FOR ARCHIVE FILTER ========= */

function pcatc_filter3_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) {
		$cutoff = '2026-01-01';
	}
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_filter3_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_filter3_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_filter3_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_filter3_is_stale($id) {
	$ts = pcatc_filter3_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_filter3_cutoff_ts();
}

function pcatc_filter3_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_filter3_is_stale((int) $vid)) {
					return true;
				}
			}
		}
		return false;
	}

	return !pcatc_filter3_is_stale((int) $product->get_id());
}

function pcatc_filter3_get_matching_product_ids($status = 'fresh') {
	$args = [
		'post_type'      => 'product',
		'post_status'    => 'publish',
		'fields'         => 'ids',
		'posts_per_page' => -1,
		'no_found_rows'  => true,
		'tax_query'      => WC()->query ? WC()->query->get_tax_query() : [],
		'meta_query'     => WC()->query ? WC()->query->get_meta_query() : [],
	];

	$product_ids = get_posts($args);
	if (empty($product_ids)) return [0];

	$matched = [];

	foreach ($product_ids as $product_id) {
		$product = wc_get_product($product_id);
		if (!$product) continue;

		$is_fresh = pcatc_filter3_product_is_fresh($product);

		if ($status === 'fresh' && $is_fresh) {
			$matched[] = $product_id;
		} elseif ($status === 'stale' && !$is_fresh) {
			$matched[] = $product_id;
		}
	}

	return !empty($matched) ? $matched : [0];
}

add_action('woocommerce_product_query', function($q) {
	if (is_admin()) return;

	$status = isset($_GET['pcatc_status']) ? sanitize_text_field(wp_unslash($_GET['pcatc_status'])) : '';
	if (!in_array($status, ['fresh', 'stale'], true)) return;

	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return;
	}

	$matched_ids = pcatc_filter3_get_matching_product_ids($status);

	$q->set('post__in', $matched_ids);
	$q->set('orderby', 'post__in');
}, 999);
محصولات بروز
TEXT - 2026-05-26 18:50:39
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */ if (!defined('ABSPATH')) exit; class PCATC_Settings_Snippet { // Options const OPT_CUTOFF = 'pcatc_cutoff_date'; const OPT_MSG = 'pcatc_message'; const OPT_FALLBACK = 'pcatc_use_modified_fallback'; const OPT_SHOW_FRONT = 'pcatc_show_front_status'; const OPT_SHOW_ADMIN = 'pcatc_show_admin_status'; const OPT_TEXT_FRESH = 'pcatc_text_fresh'; const OPT_TEXT_STALE = 'pcatc_text_stale'; // Meta const META = '_pcatc_price_last_updated'; public function __construct() { // Admin settings UI add_action('admin_menu', [$this, 'add_settings_page']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100); // Stamp when price changes add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); add_action('added_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); // Block add to cart + notices add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5); add_action('woocommerce_before_cart', [$this,'cart_checkout_notice']); add_action('woocommerce_before_checkout_form', [$this,'cart_checkout_notice']); // Front indicators add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11); add_filter('woocommerce_available_variation', [$this,'add_variation_status_data'], 10, 3); add_action('wp_enqueue_scripts', [$this,'enqueue_front_js']); // Admin list indicator add_filter('manage_edit-product_columns', [$this,'add_admin_column'], 30); add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2); add_action('admin_head', [$this,'admin_column_css']); } /* ---------- Defaults ---------- */ private function default_cutoff(): string { return '2026-01-01'; } private function default_msg(): string { return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.'; } private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; } private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; } /* ---------- Options getters ---------- */ private function get_cutoff_date(): string { $val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff()); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff(); return $val; } private function get_msg(): string { $val = (string) get_option(self::OPT_MSG, $this->default_msg()); return $val !== '' ? $val : $this->default_msg(); } private function use_fallback(): bool { return get_option(self::OPT_FALLBACK, 'yes') === 'yes'; } private function show_front(): bool { return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes'; } private function show_admin(): bool { return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes'; } private function text_fresh(): string { $val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh()); return $val !== '' ? $val : $this->default_text_fresh(); } private function text_stale(): string { $val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale()); return $val !== '' ? $val : $this->default_text_stale(); } /* ---------- Cutoff timestamp ---------- */ private function cutoff_ts(): int { $dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } /* ---------- Price update stamp ---------- */ public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void { $type = get_post_type($post_id); if (!in_array($type, ['product','product_variation'], true)) return; if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return; update_post_meta($post_id, self::META, time()); } private function last_update_ts($id): int { $ts = (int) get_post_meta($id, self::META, true); if ($ts > 0) return $ts; if ($this->use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } private function is_stale($id): bool { $ts = $this->last_update_ts($id); if ($ts <= 0) return true; return $ts < $this->cutoff_ts(); } private function status_payload_for($id): array { $stale = $this->is_stale($id); return [ 'is_stale' => $stale ? 1 : 0, 'text' => $stale ? $this->text_stale() : $this->text_fresh(), 'class' => $stale ? 'pcatc-stale' : 'pcatc-fresh', ]; } /* ---------- WooCommerce blocking ---------- */ public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) { $target_id = $variation_id ? (int)$variation_id : (int)$product_id; if ($this->is_stale($target_id)) { wc_add_notice($this->get_msg(), 'error'); return false; } return $passed; } public function cart_checkout_notice(): void { if (!function_exists('WC') || !WC()->cart) return; foreach (WC()->cart->get_cart() as $item) { $target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id']; if ($this->is_stale($target_id)) { wc_print_notice($this->get_msg(), 'error'); break; } } } /* ---------- Front status (simple + variable dynamic) ---------- */ public function render_front_status_block(): void { if (!$this->show_front() || !is_product()) return; global $product; if (!$product instanceof WC_Product) return; // For simple products, render fixed status. // For variable products, we render a container that JS will update on variation selection. $is_variable = $product->is_type('variable'); $payload = $this->status_payload_for($product->get_id()); $text = esc_html($payload['text']); $cls = esc_attr($payload['class']); echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">'; echo $is_variable ? '' : $text; echo '</div>'; } public function add_variation_status_data($variation_data, $product, $variation) { if (!$this->show_front()) return $variation_data; $vid = $variation->get_id(); $p = $this->status_payload_for($vid); $variation_data['pcatc_is_stale'] = $p['is_stale']; $variation_data['pcatc_text'] = $p['text']; $variation_data['pcatc_class'] = $p['class']; return $variation_data; } public function enqueue_front_js(): void { if (!$this->show_front() || !is_product()) return; wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true); wp_enqueue_script('pcatc-front'); // Inline CSS (front) $css = " #pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;} #pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;} "; wp_add_inline_style('woocommerce-inline', $css); // JS: update status when variation changes $js = <<<JS jQuery(function($){ var box = $('#pcatc-price-status'); if(!box.length) return; var form = $('form.variations_form'); if(!form.length) return; // simple product -> no need function setStatus(v){ if(!v || typeof v.pcatc_is_stale === 'undefined'){ // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده) box.text(''); box.removeClass('pcatc-fresh pcatc-stale'); return; } box.text(v.pcatc_text || ''); box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || ''); } form.on('found_variation', function(e, variation){ setStatus(variation); }); form.on('reset_data', function(){ setStatus(null); }); }); JS; wp_add_inline_script('pcatc-front', $js); } /* ---------- Admin list column (green/red dot) ---------- */ public function add_admin_column($columns) { if (!$this->show_admin()) return $columns; // Insert near price column if possible $new = []; foreach ($columns as $key => $label) { $new[$key] = $label; if ($key === 'price') { $new['pcatc_status'] = 'وضعیت قیمت'; } } if (!isset($new['pcatc_status'])) { $new['pcatc_status'] = 'وضعیت قیمت'; } return $new; } public function render_admin_column($column, $post_id) { if (!$this->show_admin()) return; if ($column !== 'pcatc_status') return; // For variable product: if ANY variation is fresh => green else red $product = wc_get_product($post_id); if (!$product) return; $is_fresh = false; if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; } } } } else { $is_fresh = !$this->is_stale($post_id); } echo $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>'; } public function admin_column_css() { if (!$this->show_admin()) return; echo '<style> .column-pcatc_status{width:80px;text-align:center;} .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } /* ---------- Admin settings page ---------- */ public function add_settings_page(): void { add_options_page( 'تنظیمات قفل خرید بر اساس تاریخ', 'قفل خرید (تاریخ قیمت)', 'manage_options', 'pcatc-settings', [$this, 'render_settings_page'] ); } public function register_settings(): void { register_setting('pcatc_settings_group', self::OPT_CUTOFF, [ 'type' => 'string', 'sanitize_callback' => function($v){ $v = trim((string)$v); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff(); } ]); register_setting('pcatc_settings_group', self::OPT_MSG, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_textarea_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_FALLBACK, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); } public function render_settings_page(): void { if (!current_user_can('manage_options')) return; $cutoff = esc_attr($this->get_cutoff_date()); $msg = esc_textarea($this->get_msg()); $fb = $this->use_fallback() ? 'yes' : 'no'; $sf = $this->show_front() ? 'yes' : 'no'; $sa = $this->show_admin() ? 'yes' : 'no'; $tf = esc_attr($this->text_fresh()); $ts = esc_attr($this->text_stale()); ?> <div class="wrap"> <h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1> <form method="post" action="options.php"> <?php settings_fields('pcatc_settings_group'); ?> <table class="form-table" role="presentation"> <tr> <th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th> <td> <input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>"> <p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th> <td> <textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea> <p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p> </td> </tr> <tr> <th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th> <td> <label> <input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>> اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن. </label> </td> </tr> <tr> <th scope="row">نمایش وضعیت قیمت</th> <td> <label style="display:block;margin-bottom:6px;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>> نمایش نقطه سبز/قرمز در لیست محصولات (پنل) </label> <label style="display:block;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>> نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای) </label> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;"> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;"> </td> </tr> </table> <?php submit_button('ذخیره تنظیمات'); ?> </form> </div> <?php } /* ---------- Admin bar shortcut ---------- */ public function admin_bar_link($admin_bar): void { if (!is_admin_bar_showing() || !current_user_can('manage_options')) return; $admin_bar->add_node([ 'id' => 'pcatc_settings_link', 'title' => 'تنظیمات قفل خرید', 'href' => admin_url('options-general.php?page=pcatc-settings'), ]); } } new PCATC_Settings_Snippet(); کد دوم در قسمت زیر /* === Front Dot Indicator on Archives (Shop/Category) === */ if (!defined('ABSPATH')) exit; function pcatc_dot_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01'; $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_dot_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_dot_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_dot_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_dot_is_stale($id) { $ts = pcatc_dot_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_dot_cutoff_ts(); } function pcatc_dot_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_dot_is_stale((int)$vid)) return true; } } return false; } // Simple / others: خود محصول return !pcatc_dot_is_stale((int)$product->get_id()); } /** * Add dot next to price on archives (shop/category/tag) */ function pcatc_dot_price_html($price_html, $product) { if (is_admin()) return $price_html; // فقط صفحات لیست محصولات در سایت if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return $price_html; } // اگر قیمت خالیه، چیزی نزن if (trim(wp_strip_all_tags($price_html)) === '') return $price_html; $is_fresh = pcatc_dot_product_is_fresh($product); $dot = $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>'; // نقطه + فاصله + قیمت return $dot . ' ' . $price_html; } add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2); /** CSS for dots (front) */ function pcatc_dot_css() { if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return; echo '<style> .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } add_action('wp_head', 'pcatc_dot_css', 50);
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */

if (!defined('ABSPATH')) exit;

class PCATC_Settings_Snippet {
	// Options
	const OPT_CUTOFF         = 'pcatc_cutoff_date';
	const OPT_MSG            = 'pcatc_message';
	const OPT_FALLBACK       = 'pcatc_use_modified_fallback';

	const OPT_SHOW_FRONT     = 'pcatc_show_front_status';
	const OPT_SHOW_ADMIN     = 'pcatc_show_admin_status';
	const OPT_TEXT_FRESH     = 'pcatc_text_fresh';
	const OPT_TEXT_STALE     = 'pcatc_text_stale';

	// Meta
	const META               = '_pcatc_price_last_updated';

	public function __construct() {
		// Admin settings UI
		add_action('admin_menu', [$this, 'add_settings_page']);
		add_action('admin_init', [$this, 'register_settings']);
		add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100);

		// Stamp when price changes
		add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4);
		add_action('added_post_meta',   [$this,'maybe_stamp_price_update'], 10, 4);

		// Block add to cart + notices
		add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5);
		add_action('woocommerce_before_cart',            [$this,'cart_checkout_notice']);
		add_action('woocommerce_before_checkout_form',   [$this,'cart_checkout_notice']);

		// Front indicators
		add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11);
		add_filter('woocommerce_available_variation',    [$this,'add_variation_status_data'], 10, 3);
		add_action('wp_enqueue_scripts',                 [$this,'enqueue_front_js']);

		// Admin list indicator
		add_filter('manage_edit-product_columns',        [$this,'add_admin_column'], 30);
		add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2);
		add_action('admin_head',                         [$this,'admin_column_css']);
	}

	/* ---------- Defaults ---------- */
	private function default_cutoff(): string { return '2026-01-01'; }
	private function default_msg(): string {
		return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.';
	}
	private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; }
	private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; }

	/* ---------- Options getters ---------- */
	private function get_cutoff_date(): string {
		$val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff());
		if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff();
		return $val;
	}
	private function get_msg(): string {
		$val = (string) get_option(self::OPT_MSG, $this->default_msg());
		return $val !== '' ? $val : $this->default_msg();
	}
	private function use_fallback(): bool {
		return get_option(self::OPT_FALLBACK, 'yes') === 'yes';
	}
	private function show_front(): bool {
		return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes';
	}
	private function show_admin(): bool {
		return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes';
	}
	private function text_fresh(): string {
		$val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh());
		return $val !== '' ? $val : $this->default_text_fresh();
	}
	private function text_stale(): string {
		$val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale());
		return $val !== '' ? $val : $this->default_text_stale();
	}

	/* ---------- Cutoff timestamp ---------- */
	private function cutoff_ts(): int {
		$dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone());
		return $dt->getTimestamp();
	}

	/* ---------- Price update stamp ---------- */
	public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void {
		$type = get_post_type($post_id);
		if (!in_array($type, ['product','product_variation'], true)) return;

		if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return;

		update_post_meta($post_id, self::META, time());
	}

	private function last_update_ts($id): int {
		$ts = (int) get_post_meta($id, self::META, true);
		if ($ts > 0) return $ts;

		if ($this->use_fallback()) {
			$post = get_post($id);
			if ($post && !empty($post->post_modified_gmt)) {
				$t = strtotime($post->post_modified_gmt . ' GMT');
				if ($t) return $t;
			}
		}
		return 0;
	}

	private function is_stale($id): bool {
		$ts = $this->last_update_ts($id);
		if ($ts <= 0) return true;
		return $ts < $this->cutoff_ts();
	}

	private function status_payload_for($id): array {
		$stale = $this->is_stale($id);
		return [
			'is_stale' => $stale ? 1 : 0,
			'text'     => $stale ? $this->text_stale() : $this->text_fresh(),
			'class'    => $stale ? 'pcatc-stale' : 'pcatc-fresh',
		];
	}

	/* ---------- WooCommerce blocking ---------- */
	public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) {
		$target_id = $variation_id ? (int)$variation_id : (int)$product_id;

		if ($this->is_stale($target_id)) {
			wc_add_notice($this->get_msg(), 'error');
			return false;
		}
		return $passed;
	}

	public function cart_checkout_notice(): void {
		if (!function_exists('WC') || !WC()->cart) return;

		foreach (WC()->cart->get_cart() as $item) {
			$target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id'];
			if ($this->is_stale($target_id)) {
				wc_print_notice($this->get_msg(), 'error');
				break;
			}
		}
	}

	/* ---------- Front status (simple + variable dynamic) ---------- */
	public function render_front_status_block(): void {
		if (!$this->show_front() || !is_product()) return;

		global $product;
		if (!$product instanceof WC_Product) return;

		// For simple products, render fixed status.
		// For variable products, we render a container that JS will update on variation selection.
		$is_variable = $product->is_type('variable');

		$payload = $this->status_payload_for($product->get_id());
		$text = esc_html($payload['text']);
		$cls  = esc_attr($payload['class']);

		echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">';
		echo $is_variable ? '' : $text;
		echo '</div>';
	}

	public function add_variation_status_data($variation_data, $product, $variation) {
		if (!$this->show_front()) return $variation_data;

		$vid = $variation->get_id();
		$p = $this->status_payload_for($vid);

		$variation_data['pcatc_is_stale'] = $p['is_stale'];
		$variation_data['pcatc_text']     = $p['text'];
		$variation_data['pcatc_class']    = $p['class'];

		return $variation_data;
	}

	public function enqueue_front_js(): void {
		if (!$this->show_front() || !is_product()) return;

		wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true);
		wp_enqueue_script('pcatc-front');

		// Inline CSS (front)
		$css = "
#pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;}
#pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;}
";
		wp_add_inline_style('woocommerce-inline', $css);

		// JS: update status when variation changes
		$js = <<<JS
jQuery(function($){
  var box = $('#pcatc-price-status');
  if(!box.length) return;

  var form = $('form.variations_form');
  if(!form.length) return; // simple product -> no need

  function setStatus(v){
    if(!v || typeof v.pcatc_is_stale === 'undefined'){
      // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده)
      box.text('');
      box.removeClass('pcatc-fresh pcatc-stale');
      return;
    }
    box.text(v.pcatc_text || '');
    box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || '');
  }

  form.on('found_variation', function(e, variation){
    setStatus(variation);
  });

  form.on('reset_data', function(){
    setStatus(null);
  });
});
JS;
		wp_add_inline_script('pcatc-front', $js);
	}

	/* ---------- Admin list column (green/red dot) ---------- */
	public function add_admin_column($columns) {
		if (!$this->show_admin()) return $columns;

		// Insert near price column if possible
		$new = [];
		foreach ($columns as $key => $label) {
			$new[$key] = $label;
			if ($key === 'price') {
				$new['pcatc_status'] = 'وضعیت قیمت';
			}
		}
		if (!isset($new['pcatc_status'])) {
			$new['pcatc_status'] = 'وضعیت قیمت';
		}
		return $new;
	}

	public function render_admin_column($column, $post_id) {
		if (!$this->show_admin()) return;
		if ($column !== 'pcatc_status') return;

		// For variable product: if ANY variation is fresh => green else red
		$product = wc_get_product($post_id);
		if (!$product) return;

		$is_fresh = false;

		if ($product->is_type('variable')) {
			$children = $product->get_children();
			if (!empty($children)) {
				foreach ($children as $vid) {
					if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; }
				}
			}
		} else {
			$is_fresh = !$this->is_stale($post_id);
		}

		echo $is_fresh
			? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>'
			: '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>';
	}

	public function admin_column_css() {
		if (!$this->show_admin()) return;
		echo '<style>
			.column-pcatc_status{width:80px;text-align:center;}
			.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;}
			.pcatc-dot-green{background:#19a64a;}
			.pcatc-dot-red{background:#d10000;}
		</style>';
	}

	/* ---------- Admin settings page ---------- */
	public function add_settings_page(): void {
		add_options_page(
			'تنظیمات قفل خرید بر اساس تاریخ',
			'قفل خرید (تاریخ قیمت)',
			'manage_options',
			'pcatc-settings',
			[$this, 'render_settings_page']
		);
	}

	public function register_settings(): void {
		register_setting('pcatc_settings_group', self::OPT_CUTOFF, [
			'type' => 'string',
			'sanitize_callback' => function($v){
				$v = trim((string)$v);
				return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff();
			}
		]);

		register_setting('pcatc_settings_group', self::OPT_MSG, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_textarea_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_FALLBACK, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);
	}

	public function render_settings_page(): void {
		if (!current_user_can('manage_options')) return;

		$cutoff = esc_attr($this->get_cutoff_date());
		$msg    = esc_textarea($this->get_msg());
		$fb     = $this->use_fallback() ? 'yes' : 'no';

		$sf     = $this->show_front() ? 'yes' : 'no';
		$sa     = $this->show_admin() ? 'yes' : 'no';

		$tf     = esc_attr($this->text_fresh());
		$ts     = esc_attr($this->text_stale());
		?>
		<div class="wrap">
			<h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1>
			<form method="post" action="options.php">
				<?php settings_fields('pcatc_settings_group'); ?>

				<table class="form-table" role="presentation">
					<tr>
						<th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th>
						<td>
							<input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>">
							<p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th>
						<td>
							<textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea>
							<p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th>
						<td>
							<label>
								<input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>>
								اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن.
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row">نمایش وضعیت قیمت</th>
						<td>
							<label style="display:block;margin-bottom:6px;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>>
								نمایش نقطه سبز/قرمز در لیست محصولات (پنل)
							</label>

							<label style="display:block;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>>
								نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای)
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;">
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;">
						</td>
					</tr>
				</table>

				<?php submit_button('ذخیره تنظیمات'); ?>
			</form>
		</div>
		<?php
	}

	/* ---------- Admin bar shortcut ---------- */
	public function admin_bar_link($admin_bar): void {
		if (!is_admin_bar_showing() || !current_user_can('manage_options')) return;
		$admin_bar->add_node([
			'id'    => 'pcatc_settings_link',
			'title' => 'تنظیمات قفل خرید',
			'href'  => admin_url('options-general.php?page=pcatc-settings'),
		]);
	}
}

new PCATC_Settings_Snippet();

کد دوم در قسمت زیر


/* === Front Dot Indicator on Archives (Shop/Category) === */

if (!defined('ABSPATH')) exit;

function pcatc_dot_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01';
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_dot_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_dot_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_dot_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_dot_is_stale($id) {
	$ts = pcatc_dot_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_dot_cutoff_ts();
}

function pcatc_dot_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_dot_is_stale((int)$vid)) return true;
			}
		}
		return false;
	}

	// Simple / others: خود محصول
	return !pcatc_dot_is_stale((int)$product->get_id());
}

/**
 * Add dot next to price on archives (shop/category/tag)
 */
function pcatc_dot_price_html($price_html, $product) {
	if (is_admin()) return $price_html;

	// فقط صفحات لیست محصولات در سایت
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return $price_html;
	}

	// اگر قیمت خالیه، چیزی نزن
	if (trim(wp_strip_all_tags($price_html)) === '') return $price_html;

	$is_fresh = pcatc_dot_product_is_fresh($product);
	$dot = $is_fresh
		? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>'
		: '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>';

	// نقطه + فاصله + قیمت
	return $dot . ' ' . $price_html;
}
add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2);

/** CSS for dots (front) */
function pcatc_dot_css() {
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return;

	echo '<style>
	.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);}
	.pcatc-dot-green{background:#19a64a;}
	.pcatc-dot-red{background:#d10000;}
	</style>';
}
add_action('wp_head', 'pcatc_dot_css', 50);


بی بی
TEXT - 2026-05-26 18:50:30
/* === واقعی: فیلتر نمایش فقط محصولات fresh/stale در فروشگاه و آرشیوها === */ if (!defined('ABSPATH')) exit; function pcatc_filter2_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) { $cutoff = '2026-01-01'; } $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_filter2_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_filter2_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_filter2_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_filter2_is_stale($id) { $ts = pcatc_filter2_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_filter2_cutoff_ts(); } function pcatc_filter2_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // variable: اگر حتی یکی از variationها fresh بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_filter2_is_stale((int) $vid)) { return true; } } } return false; } return !pcatc_filter2_is_stale((int) $product->get_id()); } function pcatc_filter2_get_matching_product_ids($status = 'fresh') { $args = [ 'post_type' => 'product', 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1, 'no_found_rows' => true, ]; $product_ids = get_posts($args); if (empty($product_ids)) return [0]; $matched = []; foreach ($product_ids as $product_id) { $product = wc_get_product($product_id); if (!$product) continue; $is_fresh = pcatc_filter2_product_is_fresh($product); if ($status === 'fresh' && $is_fresh) { $matched[] = $product_id; } elseif ($status === 'stale' && !$is_fresh) { $matched[] = $product_id; } } return !empty($matched) ? $matched : [0]; } add_action('pre_get_posts', function($query) { if (is_admin() || !$query->is_main_query()) return; if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return; } $status = isset($_GET['pcatc_status']) ? sanitize_text_field(wp_unslash($_GET['pcatc_status'])) : ''; if (!in_array($status, ['fresh', 'stale'], true)) return; $matched_ids = pcatc_filter2_get_matching_product_ids($status); $query->set('post__in', $matched_ids); $query->set('orderby', 'post__in'); }, 20);
/* === واقعی: فیلتر نمایش فقط محصولات fresh/stale در فروشگاه و آرشیوها === */
if (!defined('ABSPATH')) exit;

function pcatc_filter2_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) {
		$cutoff = '2026-01-01';
	}
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_filter2_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_filter2_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_filter2_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_filter2_is_stale($id) {
	$ts = pcatc_filter2_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_filter2_cutoff_ts();
}

function pcatc_filter2_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// variable: اگر حتی یکی از variationها fresh بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_filter2_is_stale((int) $vid)) {
					return true;
				}
			}
		}
		return false;
	}

	return !pcatc_filter2_is_stale((int) $product->get_id());
}

function pcatc_filter2_get_matching_product_ids($status = 'fresh') {
	$args = [
		'post_type'      => 'product',
		'post_status'    => 'publish',
		'fields'         => 'ids',
		'posts_per_page' => -1,
		'no_found_rows'  => true,
	];

	$product_ids = get_posts($args);
	if (empty($product_ids)) return [0];

	$matched = [];

	foreach ($product_ids as $product_id) {
		$product = wc_get_product($product_id);
		if (!$product) continue;

		$is_fresh = pcatc_filter2_product_is_fresh($product);

		if ($status === 'fresh' && $is_fresh) {
			$matched[] = $product_id;
		} elseif ($status === 'stale' && !$is_fresh) {
			$matched[] = $product_id;
		}
	}

	return !empty($matched) ? $matched : [0];
}

add_action('pre_get_posts', function($query) {
	if (is_admin() || !$query->is_main_query()) return;

	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return;
	}

	$status = isset($_GET['pcatc_status']) ? sanitize_text_field(wp_unslash($_GET['pcatc_status'])) : '';
	if (!in_array($status, ['fresh', 'stale'], true)) return;

	$matched_ids = pcatc_filter2_get_matching_product_ids($status);

	$query->set('post__in', $matched_ids);
	$query->set('orderby', 'post__in');
}, 20);
دسته بندی فقط محصولات قیمت بروز یعنی سبز ها
TEXT - 2026-05-26 18:50:22
/* === Filter product archives by PCATC status: fresh/stale === */ if (!defined('ABSPATH')) exit; function pcatc_filter_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) { $cutoff = '2026-01-01'; } $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_filter_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_filter_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_filter_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_filter_is_stale($id) { $ts = pcatc_filter_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_filter_cutoff_ts(); } function pcatc_filter_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // Variable: اگر حداقل یک variation تازه بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_filter_is_stale((int) $vid)) { return true; } } } return false; } // Simple / others return !pcatc_filter_is_stale((int) $product->get_id()); } add_action('pre_get_posts', function($query) { if (is_admin() || !$query->is_main_query()) return; // فقط در آرشیو محصولات / دسته‌بندی / تگ if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return; } $status = isset($_GET['pcatc_status']) ? sanitize_text_field(wp_unslash($_GET['pcatc_status'])) : ''; if (!in_array($status, ['fresh', 'stale'], true)) return; // همه محصولات صفحه را بگیریم تا بعداً با post__in محدود کنیم $query->set('posts_per_page', -1); add_filter('posts_results', function($posts, $q) use ($status) { if (is_admin() || !$q->is_main_query()) return $posts; if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return $posts; } $filtered_ids = []; foreach ($posts as $post) { $product = wc_get_product($post->ID); if (!$product) continue; $is_fresh = pcatc_filter_product_is_fresh($product); if ($status === 'fresh' && $is_fresh) { $filtered_ids[] = $post->ID; } if ($status === 'stale' && !$is_fresh) { $filtered_ids[] = $post->ID; } } if (empty($filtered_ids)) { $q->set('post__in', [0]); } else { $q->set('post__in', $filtered_ids); } return $posts; }, 10, 2); });
/* === Filter product archives by PCATC status: fresh/stale === */
if (!defined('ABSPATH')) exit;

function pcatc_filter_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) {
		$cutoff = '2026-01-01';
	}
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_filter_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_filter_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_filter_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_filter_is_stale($id) {
	$ts = pcatc_filter_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_filter_cutoff_ts();
}

function pcatc_filter_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// Variable: اگر حداقل یک variation تازه بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_filter_is_stale((int) $vid)) {
					return true;
				}
			}
		}
		return false;
	}

	// Simple / others
	return !pcatc_filter_is_stale((int) $product->get_id());
}

add_action('pre_get_posts', function($query) {
	if (is_admin() || !$query->is_main_query()) return;

	// فقط در آرشیو محصولات / دسته‌بندی / تگ
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return;
	}

	$status = isset($_GET['pcatc_status']) ? sanitize_text_field(wp_unslash($_GET['pcatc_status'])) : '';
	if (!in_array($status, ['fresh', 'stale'], true)) return;

	// همه محصولات صفحه را بگیریم تا بعداً با post__in محدود کنیم
	$query->set('posts_per_page', -1);

	add_filter('posts_results', function($posts, $q) use ($status) {
		if (is_admin() || !$q->is_main_query()) return $posts;

		if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
			return $posts;
		}

		$filtered_ids = [];

		foreach ($posts as $post) {
			$product = wc_get_product($post->ID);
			if (!$product) continue;

			$is_fresh = pcatc_filter_product_is_fresh($product);

			if ($status === 'fresh' && $is_fresh) {
				$filtered_ids[] = $post->ID;
			}

			if ($status === 'stale' && !$is_fresh) {
				$filtered_ids[] = $post->ID;
			}
		}

		if (empty($filtered_ids)) {
			$q->set('post__in', [0]);
		} else {
			$q->set('post__in', $filtered_ids);
		}

		return $posts;
	}, 10, 2);
});
محصولات بروز
TEXT - 2026-05-26 18:41:00
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */ if (!defined('ABSPATH')) exit; class PCATC_Settings_Snippet { // Options const OPT_CUTOFF = 'pcatc_cutoff_date'; const OPT_MSG = 'pcatc_message'; const OPT_FALLBACK = 'pcatc_use_modified_fallback'; const OPT_SHOW_FRONT = 'pcatc_show_front_status'; const OPT_SHOW_ADMIN = 'pcatc_show_admin_status'; const OPT_TEXT_FRESH = 'pcatc_text_fresh'; const OPT_TEXT_STALE = 'pcatc_text_stale'; // Meta const META = '_pcatc_price_last_updated'; public function __construct() { // Admin settings UI add_action('admin_menu', [$this, 'add_settings_page']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100); // Stamp when price changes add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); add_action('added_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); // Block add to cart + notices add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5); add_action('woocommerce_before_cart', [$this,'cart_checkout_notice']); add_action('woocommerce_before_checkout_form', [$this,'cart_checkout_notice']); // Front indicators add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11); add_filter('woocommerce_available_variation', [$this,'add_variation_status_data'], 10, 3); add_action('wp_enqueue_scripts', [$this,'enqueue_front_js']); // Admin list indicator add_filter('manage_edit-product_columns', [$this,'add_admin_column'], 30); add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2); add_action('admin_head', [$this,'admin_column_css']); } /* ---------- Defaults ---------- */ private function default_cutoff(): string { return '2026-01-01'; } private function default_msg(): string { return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.'; } private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; } private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; } /* ---------- Options getters ---------- */ private function get_cutoff_date(): string { $val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff()); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff(); return $val; } private function get_msg(): string { $val = (string) get_option(self::OPT_MSG, $this->default_msg()); return $val !== '' ? $val : $this->default_msg(); } private function use_fallback(): bool { return get_option(self::OPT_FALLBACK, 'yes') === 'yes'; } private function show_front(): bool { return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes'; } private function show_admin(): bool { return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes'; } private function text_fresh(): string { $val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh()); return $val !== '' ? $val : $this->default_text_fresh(); } private function text_stale(): string { $val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale()); return $val !== '' ? $val : $this->default_text_stale(); } /* ---------- Cutoff timestamp ---------- */ private function cutoff_ts(): int { $dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } /* ---------- Price update stamp ---------- */ public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void { $type = get_post_type($post_id); if (!in_array($type, ['product','product_variation'], true)) return; if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return; update_post_meta($post_id, self::META, time()); } private function last_update_ts($id): int { $ts = (int) get_post_meta($id, self::META, true); if ($ts > 0) return $ts; if ($this->use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } private function is_stale($id): bool { $ts = $this->last_update_ts($id); if ($ts <= 0) return true; return $ts < $this->cutoff_ts(); } private function status_payload_for($id): array { $stale = $this->is_stale($id); return [ 'is_stale' => $stale ? 1 : 0, 'text' => $stale ? $this->text_stale() : $this->text_fresh(), 'class' => $stale ? 'pcatc-stale' : 'pcatc-fresh', ]; } /* ---------- WooCommerce blocking ---------- */ public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) { $target_id = $variation_id ? (int)$variation_id : (int)$product_id; if ($this->is_stale($target_id)) { wc_add_notice($this->get_msg(), 'error'); return false; } return $passed; } public function cart_checkout_notice(): void { if (!function_exists('WC') || !WC()->cart) return; foreach (WC()->cart->get_cart() as $item) { $target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id']; if ($this->is_stale($target_id)) { wc_print_notice($this->get_msg(), 'error'); break; } } } /* ---------- Front status (simple + variable dynamic) ---------- */ public function render_front_status_block(): void { if (!$this->show_front() || !is_product()) return; global $product; if (!$product instanceof WC_Product) return; // For simple products, render fixed status. // For variable products, we render a container that JS will update on variation selection. $is_variable = $product->is_type('variable'); $payload = $this->status_payload_for($product->get_id()); $text = esc_html($payload['text']); $cls = esc_attr($payload['class']); echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">'; echo $is_variable ? '' : $text; echo '</div>'; } public function add_variation_status_data($variation_data, $product, $variation) { if (!$this->show_front()) return $variation_data; $vid = $variation->get_id(); $p = $this->status_payload_for($vid); $variation_data['pcatc_is_stale'] = $p['is_stale']; $variation_data['pcatc_text'] = $p['text']; $variation_data['pcatc_class'] = $p['class']; return $variation_data; } public function enqueue_front_js(): void { if (!$this->show_front() || !is_product()) return; wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true); wp_enqueue_script('pcatc-front'); // Inline CSS (front) $css = " #pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;} #pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;} "; wp_add_inline_style('woocommerce-inline', $css); // JS: update status when variation changes $js = <<<JS jQuery(function($){ var box = $('#pcatc-price-status'); if(!box.length) return; var form = $('form.variations_form'); if(!form.length) return; // simple product -> no need function setStatus(v){ if(!v || typeof v.pcatc_is_stale === 'undefined'){ // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده) box.text(''); box.removeClass('pcatc-fresh pcatc-stale'); return; } box.text(v.pcatc_text || ''); box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || ''); } form.on('found_variation', function(e, variation){ setStatus(variation); }); form.on('reset_data', function(){ setStatus(null); }); }); JS; wp_add_inline_script('pcatc-front', $js); } /* ---------- Admin list column (green/red dot) ---------- */ public function add_admin_column($columns) { if (!$this->show_admin()) return $columns; // Insert near price column if possible $new = []; foreach ($columns as $key => $label) { $new[$key] = $label; if ($key === 'price') { $new['pcatc_status'] = 'وضعیت قیمت'; } } if (!isset($new['pcatc_status'])) { $new['pcatc_status'] = 'وضعیت قیمت'; } return $new; } public function render_admin_column($column, $post_id) { if (!$this->show_admin()) return; if ($column !== 'pcatc_status') return; // For variable product: if ANY variation is fresh => green else red $product = wc_get_product($post_id); if (!$product) return; $is_fresh = false; if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; } } } } else { $is_fresh = !$this->is_stale($post_id); } echo $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>'; } public function admin_column_css() { if (!$this->show_admin()) return; echo '<style> .column-pcatc_status{width:80px;text-align:center;} .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } /* ---------- Admin settings page ---------- */ public function add_settings_page(): void { add_options_page( 'تنظیمات قفل خرید بر اساس تاریخ', 'قفل خرید (تاریخ قیمت)', 'manage_options', 'pcatc-settings', [$this, 'render_settings_page'] ); } public function register_settings(): void { register_setting('pcatc_settings_group', self::OPT_CUTOFF, [ 'type' => 'string', 'sanitize_callback' => function($v){ $v = trim((string)$v); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff(); } ]); register_setting('pcatc_settings_group', self::OPT_MSG, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_textarea_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_FALLBACK, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); } public function render_settings_page(): void { if (!current_user_can('manage_options')) return; $cutoff = esc_attr($this->get_cutoff_date()); $msg = esc_textarea($this->get_msg()); $fb = $this->use_fallback() ? 'yes' : 'no'; $sf = $this->show_front() ? 'yes' : 'no'; $sa = $this->show_admin() ? 'yes' : 'no'; $tf = esc_attr($this->text_fresh()); $ts = esc_attr($this->text_stale()); ?> <div class="wrap"> <h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1> <form method="post" action="options.php"> <?php settings_fields('pcatc_settings_group'); ?> <table class="form-table" role="presentation"> <tr> <th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th> <td> <input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>"> <p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th> <td> <textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea> <p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p> </td> </tr> <tr> <th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th> <td> <label> <input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>> اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن. </label> </td> </tr> <tr> <th scope="row">نمایش وضعیت قیمت</th> <td> <label style="display:block;margin-bottom:6px;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>> نمایش نقطه سبز/قرمز در لیست محصولات (پنل) </label> <label style="display:block;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>> نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای) </label> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;"> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;"> </td> </tr> </table> <?php submit_button('ذخیره تنظیمات'); ?> </form> </div> <?php } /* ---------- Admin bar shortcut ---------- */ public function admin_bar_link($admin_bar): void { if (!is_admin_bar_showing() || !current_user_can('manage_options')) return; $admin_bar->add_node([ 'id' => 'pcatc_settings_link', 'title' => 'تنظیمات قفل خرید', 'href' => admin_url('options-general.php?page=pcatc-settings'), ]); } } new PCATC_Settings_Snippet(); کد دوم در قسمت زیر /* === Front Dot Indicator on Archives (Shop/Category) === */ if (!defined('ABSPATH')) exit; function pcatc_dot_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01'; $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_dot_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_dot_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_dot_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_dot_is_stale($id) { $ts = pcatc_dot_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_dot_cutoff_ts(); } function pcatc_dot_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_dot_is_stale((int)$vid)) return true; } } return false; } // Simple / others: خود محصول return !pcatc_dot_is_stale((int)$product->get_id()); } /** * Add dot next to price on archives (shop/category/tag) */ function pcatc_dot_price_html($price_html, $product) { if (is_admin()) return $price_html; // فقط صفحات لیست محصولات در سایت if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return $price_html; } // اگر قیمت خالیه، چیزی نزن if (trim(wp_strip_all_tags($price_html)) === '') return $price_html; $is_fresh = pcatc_dot_product_is_fresh($product); $dot = $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>'; // نقطه + فاصله + قیمت return $dot . ' ' . $price_html; } add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2); /** CSS for dots (front) */ function pcatc_dot_css() { if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return; echo '<style> .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } add_action('wp_head', 'pcatc_dot_css', 50);
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */

if (!defined('ABSPATH')) exit;

class PCATC_Settings_Snippet {
	// Options
	const OPT_CUTOFF         = 'pcatc_cutoff_date';
	const OPT_MSG            = 'pcatc_message';
	const OPT_FALLBACK       = 'pcatc_use_modified_fallback';

	const OPT_SHOW_FRONT     = 'pcatc_show_front_status';
	const OPT_SHOW_ADMIN     = 'pcatc_show_admin_status';
	const OPT_TEXT_FRESH     = 'pcatc_text_fresh';
	const OPT_TEXT_STALE     = 'pcatc_text_stale';

	// Meta
	const META               = '_pcatc_price_last_updated';

	public function __construct() {
		// Admin settings UI
		add_action('admin_menu', [$this, 'add_settings_page']);
		add_action('admin_init', [$this, 'register_settings']);
		add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100);

		// Stamp when price changes
		add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4);
		add_action('added_post_meta',   [$this,'maybe_stamp_price_update'], 10, 4);

		// Block add to cart + notices
		add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5);
		add_action('woocommerce_before_cart',            [$this,'cart_checkout_notice']);
		add_action('woocommerce_before_checkout_form',   [$this,'cart_checkout_notice']);

		// Front indicators
		add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11);
		add_filter('woocommerce_available_variation',    [$this,'add_variation_status_data'], 10, 3);
		add_action('wp_enqueue_scripts',                 [$this,'enqueue_front_js']);

		// Admin list indicator
		add_filter('manage_edit-product_columns',        [$this,'add_admin_column'], 30);
		add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2);
		add_action('admin_head',                         [$this,'admin_column_css']);
	}

	/* ---------- Defaults ---------- */
	private function default_cutoff(): string { return '2026-01-01'; }
	private function default_msg(): string {
		return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.';
	}
	private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; }
	private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; }

	/* ---------- Options getters ---------- */
	private function get_cutoff_date(): string {
		$val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff());
		if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff();
		return $val;
	}
	private function get_msg(): string {
		$val = (string) get_option(self::OPT_MSG, $this->default_msg());
		return $val !== '' ? $val : $this->default_msg();
	}
	private function use_fallback(): bool {
		return get_option(self::OPT_FALLBACK, 'yes') === 'yes';
	}
	private function show_front(): bool {
		return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes';
	}
	private function show_admin(): bool {
		return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes';
	}
	private function text_fresh(): string {
		$val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh());
		return $val !== '' ? $val : $this->default_text_fresh();
	}
	private function text_stale(): string {
		$val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale());
		return $val !== '' ? $val : $this->default_text_stale();
	}

	/* ---------- Cutoff timestamp ---------- */
	private function cutoff_ts(): int {
		$dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone());
		return $dt->getTimestamp();
	}

	/* ---------- Price update stamp ---------- */
	public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void {
		$type = get_post_type($post_id);
		if (!in_array($type, ['product','product_variation'], true)) return;

		if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return;

		update_post_meta($post_id, self::META, time());
	}

	private function last_update_ts($id): int {
		$ts = (int) get_post_meta($id, self::META, true);
		if ($ts > 0) return $ts;

		if ($this->use_fallback()) {
			$post = get_post($id);
			if ($post && !empty($post->post_modified_gmt)) {
				$t = strtotime($post->post_modified_gmt . ' GMT');
				if ($t) return $t;
			}
		}
		return 0;
	}

	private function is_stale($id): bool {
		$ts = $this->last_update_ts($id);
		if ($ts <= 0) return true;
		return $ts < $this->cutoff_ts();
	}

	private function status_payload_for($id): array {
		$stale = $this->is_stale($id);
		return [
			'is_stale' => $stale ? 1 : 0,
			'text'     => $stale ? $this->text_stale() : $this->text_fresh(),
			'class'    => $stale ? 'pcatc-stale' : 'pcatc-fresh',
		];
	}

	/* ---------- WooCommerce blocking ---------- */
	public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) {
		$target_id = $variation_id ? (int)$variation_id : (int)$product_id;

		if ($this->is_stale($target_id)) {
			wc_add_notice($this->get_msg(), 'error');
			return false;
		}
		return $passed;
	}

	public function cart_checkout_notice(): void {
		if (!function_exists('WC') || !WC()->cart) return;

		foreach (WC()->cart->get_cart() as $item) {
			$target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id'];
			if ($this->is_stale($target_id)) {
				wc_print_notice($this->get_msg(), 'error');
				break;
			}
		}
	}

	/* ---------- Front status (simple + variable dynamic) ---------- */
	public function render_front_status_block(): void {
		if (!$this->show_front() || !is_product()) return;

		global $product;
		if (!$product instanceof WC_Product) return;

		// For simple products, render fixed status.
		// For variable products, we render a container that JS will update on variation selection.
		$is_variable = $product->is_type('variable');

		$payload = $this->status_payload_for($product->get_id());
		$text = esc_html($payload['text']);
		$cls  = esc_attr($payload['class']);

		echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">';
		echo $is_variable ? '' : $text;
		echo '</div>';
	}

	public function add_variation_status_data($variation_data, $product, $variation) {
		if (!$this->show_front()) return $variation_data;

		$vid = $variation->get_id();
		$p = $this->status_payload_for($vid);

		$variation_data['pcatc_is_stale'] = $p['is_stale'];
		$variation_data['pcatc_text']     = $p['text'];
		$variation_data['pcatc_class']    = $p['class'];

		return $variation_data;
	}

	public function enqueue_front_js(): void {
		if (!$this->show_front() || !is_product()) return;

		wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true);
		wp_enqueue_script('pcatc-front');

		// Inline CSS (front)
		$css = "
#pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;}
#pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;}
";
		wp_add_inline_style('woocommerce-inline', $css);

		// JS: update status when variation changes
		$js = <<<JS
jQuery(function($){
  var box = $('#pcatc-price-status');
  if(!box.length) return;

  var form = $('form.variations_form');
  if(!form.length) return; // simple product -> no need

  function setStatus(v){
    if(!v || typeof v.pcatc_is_stale === 'undefined'){
      // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده)
      box.text('');
      box.removeClass('pcatc-fresh pcatc-stale');
      return;
    }
    box.text(v.pcatc_text || '');
    box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || '');
  }

  form.on('found_variation', function(e, variation){
    setStatus(variation);
  });

  form.on('reset_data', function(){
    setStatus(null);
  });
});
JS;
		wp_add_inline_script('pcatc-front', $js);
	}

	/* ---------- Admin list column (green/red dot) ---------- */
	public function add_admin_column($columns) {
		if (!$this->show_admin()) return $columns;

		// Insert near price column if possible
		$new = [];
		foreach ($columns as $key => $label) {
			$new[$key] = $label;
			if ($key === 'price') {
				$new['pcatc_status'] = 'وضعیت قیمت';
			}
		}
		if (!isset($new['pcatc_status'])) {
			$new['pcatc_status'] = 'وضعیت قیمت';
		}
		return $new;
	}

	public function render_admin_column($column, $post_id) {
		if (!$this->show_admin()) return;
		if ($column !== 'pcatc_status') return;

		// For variable product: if ANY variation is fresh => green else red
		$product = wc_get_product($post_id);
		if (!$product) return;

		$is_fresh = false;

		if ($product->is_type('variable')) {
			$children = $product->get_children();
			if (!empty($children)) {
				foreach ($children as $vid) {
					if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; }
				}
			}
		} else {
			$is_fresh = !$this->is_stale($post_id);
		}

		echo $is_fresh
			? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>'
			: '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>';
	}

	public function admin_column_css() {
		if (!$this->show_admin()) return;
		echo '<style>
			.column-pcatc_status{width:80px;text-align:center;}
			.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;}
			.pcatc-dot-green{background:#19a64a;}
			.pcatc-dot-red{background:#d10000;}
		</style>';
	}

	/* ---------- Admin settings page ---------- */
	public function add_settings_page(): void {
		add_options_page(
			'تنظیمات قفل خرید بر اساس تاریخ',
			'قفل خرید (تاریخ قیمت)',
			'manage_options',
			'pcatc-settings',
			[$this, 'render_settings_page']
		);
	}

	public function register_settings(): void {
		register_setting('pcatc_settings_group', self::OPT_CUTOFF, [
			'type' => 'string',
			'sanitize_callback' => function($v){
				$v = trim((string)$v);
				return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff();
			}
		]);

		register_setting('pcatc_settings_group', self::OPT_MSG, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_textarea_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_FALLBACK, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);
	}

	public function render_settings_page(): void {
		if (!current_user_can('manage_options')) return;

		$cutoff = esc_attr($this->get_cutoff_date());
		$msg    = esc_textarea($this->get_msg());
		$fb     = $this->use_fallback() ? 'yes' : 'no';

		$sf     = $this->show_front() ? 'yes' : 'no';
		$sa     = $this->show_admin() ? 'yes' : 'no';

		$tf     = esc_attr($this->text_fresh());
		$ts     = esc_attr($this->text_stale());
		?>
		<div class="wrap">
			<h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1>
			<form method="post" action="options.php">
				<?php settings_fields('pcatc_settings_group'); ?>

				<table class="form-table" role="presentation">
					<tr>
						<th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th>
						<td>
							<input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>">
							<p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th>
						<td>
							<textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea>
							<p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th>
						<td>
							<label>
								<input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>>
								اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن.
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row">نمایش وضعیت قیمت</th>
						<td>
							<label style="display:block;margin-bottom:6px;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>>
								نمایش نقطه سبز/قرمز در لیست محصولات (پنل)
							</label>

							<label style="display:block;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>>
								نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای)
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;">
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;">
						</td>
					</tr>
				</table>

				<?php submit_button('ذخیره تنظیمات'); ?>
			</form>
		</div>
		<?php
	}

	/* ---------- Admin bar shortcut ---------- */
	public function admin_bar_link($admin_bar): void {
		if (!is_admin_bar_showing() || !current_user_can('manage_options')) return;
		$admin_bar->add_node([
			'id'    => 'pcatc_settings_link',
			'title' => 'تنظیمات قفل خرید',
			'href'  => admin_url('options-general.php?page=pcatc-settings'),
		]);
	}
}

new PCATC_Settings_Snippet();

کد دوم در قسمت زیر


/* === Front Dot Indicator on Archives (Shop/Category) === */

if (!defined('ABSPATH')) exit;

function pcatc_dot_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01';
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_dot_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_dot_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_dot_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_dot_is_stale($id) {
	$ts = pcatc_dot_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_dot_cutoff_ts();
}

function pcatc_dot_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_dot_is_stale((int)$vid)) return true;
			}
		}
		return false;
	}

	// Simple / others: خود محصول
	return !pcatc_dot_is_stale((int)$product->get_id());
}

/**
 * Add dot next to price on archives (shop/category/tag)
 */
function pcatc_dot_price_html($price_html, $product) {
	if (is_admin()) return $price_html;

	// فقط صفحات لیست محصولات در سایت
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return $price_html;
	}

	// اگر قیمت خالیه، چیزی نزن
	if (trim(wp_strip_all_tags($price_html)) === '') return $price_html;

	$is_fresh = pcatc_dot_product_is_fresh($product);
	$dot = $is_fresh
		? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>'
		: '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>';

	// نقطه + فاصله + قیمت
	return $dot . ' ' . $price_html;
}
add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2);

/** CSS for dots (front) */
function pcatc_dot_css() {
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return;

	echo '<style>
	.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);}
	.pcatc-dot-green{background:#19a64a;}
	.pcatc-dot-red{background:#d10000;}
	</style>';
}
add_action('wp_head', 'pcatc_dot_css', 50);


محصولات بروز
TEXT - 2026-05-26 18:39:30
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */ if (!defined('ABSPATH')) exit; class PCATC_Settings_Snippet { // Options const OPT_CUTOFF = 'pcatc_cutoff_date'; const OPT_MSG = 'pcatc_message'; const OPT_FALLBACK = 'pcatc_use_modified_fallback'; const OPT_SHOW_FRONT = 'pcatc_show_front_status'; const OPT_SHOW_ADMIN = 'pcatc_show_admin_status'; const OPT_TEXT_FRESH = 'pcatc_text_fresh'; const OPT_TEXT_STALE = 'pcatc_text_stale'; // Meta const META = '_pcatc_price_last_updated'; public function __construct() { // Admin settings UI add_action('admin_menu', [$this, 'add_settings_page']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100); // Stamp when price changes add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); add_action('added_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); // Block add to cart + notices add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5); add_action('woocommerce_before_cart', [$this,'cart_checkout_notice']); add_action('woocommerce_before_checkout_form', [$this,'cart_checkout_notice']); // Front indicators add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11); add_filter('woocommerce_available_variation', [$this,'add_variation_status_data'], 10, 3); add_action('wp_enqueue_scripts', [$this,'enqueue_front_js']); // Admin list indicator add_filter('manage_edit-product_columns', [$this,'add_admin_column'], 30); add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2); add_action('admin_head', [$this,'admin_column_css']); } /* ---------- Defaults ---------- */ private function default_cutoff(): string { return '2026-01-01'; } private function default_msg(): string { return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.'; } private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; } private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; } /* ---------- Options getters ---------- */ private function get_cutoff_date(): string { $val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff()); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff(); return $val; } private function get_msg(): string { $val = (string) get_option(self::OPT_MSG, $this->default_msg()); return $val !== '' ? $val : $this->default_msg(); } private function use_fallback(): bool { return get_option(self::OPT_FALLBACK, 'yes') === 'yes'; } private function show_front(): bool { return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes'; } private function show_admin(): bool { return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes'; } private function text_fresh(): string { $val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh()); return $val !== '' ? $val : $this->default_text_fresh(); } private function text_stale(): string { $val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale()); return $val !== '' ? $val : $this->default_text_stale(); } /* ---------- Cutoff timestamp ---------- */ private function cutoff_ts(): int { $dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } /* ---------- Price update stamp ---------- */ public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void { $type = get_post_type($post_id); if (!in_array($type, ['product','product_variation'], true)) return; if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return; update_post_meta($post_id, self::META, time()); } private function last_update_ts($id): int { $ts = (int) get_post_meta($id, self::META, true); if ($ts > 0) return $ts; if ($this->use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } private function is_stale($id): bool { $ts = $this->last_update_ts($id); if ($ts <= 0) return true; return $ts < $this->cutoff_ts(); } private function status_payload_for($id): array { $stale = $this->is_stale($id); return [ 'is_stale' => $stale ? 1 : 0, 'text' => $stale ? $this->text_stale() : $this->text_fresh(), 'class' => $stale ? 'pcatc-stale' : 'pcatc-fresh', ]; } /* ---------- WooCommerce blocking ---------- */ public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) { $target_id = $variation_id ? (int)$variation_id : (int)$product_id; if ($this->is_stale($target_id)) { wc_add_notice($this->get_msg(), 'error'); return false; } return $passed; } public function cart_checkout_notice(): void { if (!function_exists('WC') || !WC()->cart) return; foreach (WC()->cart->get_cart() as $item) { $target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id']; if ($this->is_stale($target_id)) { wc_print_notice($this->get_msg(), 'error'); break; } } } /* ---------- Front status (simple + variable dynamic) ---------- */ public function render_front_status_block(): void { if (!$this->show_front() || !is_product()) return; global $product; if (!$product instanceof WC_Product) return; // For simple products, render fixed status. // For variable products, we render a container that JS will update on variation selection. $is_variable = $product->is_type('variable'); $payload = $this->status_payload_for($product->get_id()); $text = esc_html($payload['text']); $cls = esc_attr($payload['class']); echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">'; echo $is_variable ? '' : $text; echo '</div>'; } public function add_variation_status_data($variation_data, $product, $variation) { if (!$this->show_front()) return $variation_data; $vid = $variation->get_id(); $p = $this->status_payload_for($vid); $variation_data['pcatc_is_stale'] = $p['is_stale']; $variation_data['pcatc_text'] = $p['text']; $variation_data['pcatc_class'] = $p['class']; return $variation_data; } public function enqueue_front_js(): void { if (!$this->show_front() || !is_product()) return; wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true); wp_enqueue_script('pcatc-front'); // Inline CSS (front) $css = " #pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;} #pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;} "; wp_add_inline_style('woocommerce-inline', $css); // JS: update status when variation changes $js = <<<JS jQuery(function($){ var box = $('#pcatc-price-status'); if(!box.length) return; var form = $('form.variations_form'); if(!form.length) return; // simple product -> no need function setStatus(v){ if(!v || typeof v.pcatc_is_stale === 'undefined'){ // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده) box.text(''); box.removeClass('pcatc-fresh pcatc-stale'); return; } box.text(v.pcatc_text || ''); box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || ''); } form.on('found_variation', function(e, variation){ setStatus(variation); }); form.on('reset_data', function(){ setStatus(null); }); }); JS; wp_add_inline_script('pcatc-front', $js); } /* ---------- Admin list column (green/red dot) ---------- */ public function add_admin_column($columns) { if (!$this->show_admin()) return $columns; // Insert near price column if possible $new = []; foreach ($columns as $key => $label) { $new[$key] = $label; if ($key === 'price') { $new['pcatc_status'] = 'وضعیت قیمت'; } } if (!isset($new['pcatc_status'])) { $new['pcatc_status'] = 'وضعیت قیمت'; } return $new; } public function render_admin_column($column, $post_id) { if (!$this->show_admin()) return; if ($column !== 'pcatc_status') return; // For variable product: if ANY variation is fresh => green else red $product = wc_get_product($post_id); if (!$product) return; $is_fresh = false; if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; } } } } else { $is_fresh = !$this->is_stale($post_id); } echo $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>'; } public function admin_column_css() { if (!$this->show_admin()) return; echo '<style> .column-pcatc_status{width:80px;text-align:center;} .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } /* ---------- Admin settings page ---------- */ public function add_settings_page(): void { add_options_page( 'تنظیمات قفل خرید بر اساس تاریخ', 'قفل خرید (تاریخ قیمت)', 'manage_options', 'pcatc-settings', [$this, 'render_settings_page'] ); } public function register_settings(): void { register_setting('pcatc_settings_group', self::OPT_CUTOFF, [ 'type' => 'string', 'sanitize_callback' => function($v){ $v = trim((string)$v); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff(); } ]); register_setting('pcatc_settings_group', self::OPT_MSG, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_textarea_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_FALLBACK, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); } public function render_settings_page(): void { if (!current_user_can('manage_options')) return; $cutoff = esc_attr($this->get_cutoff_date()); $msg = esc_textarea($this->get_msg()); $fb = $this->use_fallback() ? 'yes' : 'no'; $sf = $this->show_front() ? 'yes' : 'no'; $sa = $this->show_admin() ? 'yes' : 'no'; $tf = esc_attr($this->text_fresh()); $ts = esc_attr($this->text_stale()); ?> <div class="wrap"> <h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1> <form method="post" action="options.php"> <?php settings_fields('pcatc_settings_group'); ?> <table class="form-table" role="presentation"> <tr> <th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th> <td> <input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>"> <p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th> <td> <textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea> <p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p> </td> </tr> <tr> <th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th> <td> <label> <input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>> اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن. </label> </td> </tr> <tr> <th scope="row">نمایش وضعیت قیمت</th> <td> <label style="display:block;margin-bottom:6px;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>> نمایش نقطه سبز/قرمز در لیست محصولات (پنل) </label> <label style="display:block;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>> نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای) </label> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;"> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;"> </td> </tr> </table> <?php submit_button('ذخیره تنظیمات'); ?> </form> </div> <?php } /* ---------- Admin bar shortcut ---------- */ public function admin_bar_link($admin_bar): void { if (!is_admin_bar_showing() || !current_user_can('manage_options')) return; $admin_bar->add_node([ 'id' => 'pcatc_settings_link', 'title' => 'تنظیمات قفل خرید', 'href' => admin_url('options-general.php?page=pcatc-settings'), ]); } } new PCATC_Settings_Snippet(); کد دوم در قسمت زیر /* === Front Dot Indicator on Archives (Shop/Category) === */ if (!defined('ABSPATH')) exit; function pcatc_dot_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01'; $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_dot_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_dot_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_dot_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_dot_is_stale($id) { $ts = pcatc_dot_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_dot_cutoff_ts(); } function pcatc_dot_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_dot_is_stale((int)$vid)) return true; } } return false; } // Simple / others: خود محصول return !pcatc_dot_is_stale((int)$product->get_id()); } /** * Add dot next to price on archives (shop/category/tag) */ function pcatc_dot_price_html($price_html, $product) { if (is_admin()) return $price_html; // فقط صفحات لیست محصولات در سایت if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return $price_html; } // اگر قیمت خالیه، چیزی نزن if (trim(wp_strip_all_tags($price_html)) === '') return $price_html; $is_fresh = pcatc_dot_product_is_fresh($product); $dot = $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>'; // نقطه + فاصله + قیمت return $dot . ' ' . $price_html; } add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2); /** CSS for dots (front) */ function pcatc_dot_css() { if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return; echo '<style> .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } add_action('wp_head', 'pcatc_dot_css', 50);
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */

if (!defined('ABSPATH')) exit;

class PCATC_Settings_Snippet {
	// Options
	const OPT_CUTOFF         = 'pcatc_cutoff_date';
	const OPT_MSG            = 'pcatc_message';
	const OPT_FALLBACK       = 'pcatc_use_modified_fallback';

	const OPT_SHOW_FRONT     = 'pcatc_show_front_status';
	const OPT_SHOW_ADMIN     = 'pcatc_show_admin_status';
	const OPT_TEXT_FRESH     = 'pcatc_text_fresh';
	const OPT_TEXT_STALE     = 'pcatc_text_stale';

	// Meta
	const META               = '_pcatc_price_last_updated';

	public function __construct() {
		// Admin settings UI
		add_action('admin_menu', [$this, 'add_settings_page']);
		add_action('admin_init', [$this, 'register_settings']);
		add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100);

		// Stamp when price changes
		add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4);
		add_action('added_post_meta',   [$this,'maybe_stamp_price_update'], 10, 4);

		// Block add to cart + notices
		add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5);
		add_action('woocommerce_before_cart',            [$this,'cart_checkout_notice']);
		add_action('woocommerce_before_checkout_form',   [$this,'cart_checkout_notice']);

		// Front indicators
		add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11);
		add_filter('woocommerce_available_variation',    [$this,'add_variation_status_data'], 10, 3);
		add_action('wp_enqueue_scripts',                 [$this,'enqueue_front_js']);

		// Admin list indicator
		add_filter('manage_edit-product_columns',        [$this,'add_admin_column'], 30);
		add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2);
		add_action('admin_head',                         [$this,'admin_column_css']);
	}

	/* ---------- Defaults ---------- */
	private function default_cutoff(): string { return '2026-01-01'; }
	private function default_msg(): string {
		return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.';
	}
	private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; }
	private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; }

	/* ---------- Options getters ---------- */
	private function get_cutoff_date(): string {
		$val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff());
		if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff();
		return $val;
	}
	private function get_msg(): string {
		$val = (string) get_option(self::OPT_MSG, $this->default_msg());
		return $val !== '' ? $val : $this->default_msg();
	}
	private function use_fallback(): bool {
		return get_option(self::OPT_FALLBACK, 'yes') === 'yes';
	}
	private function show_front(): bool {
		return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes';
	}
	private function show_admin(): bool {
		return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes';
	}
	private function text_fresh(): string {
		$val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh());
		return $val !== '' ? $val : $this->default_text_fresh();
	}
	private function text_stale(): string {
		$val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale());
		return $val !== '' ? $val : $this->default_text_stale();
	}

	/* ---------- Cutoff timestamp ---------- */
	private function cutoff_ts(): int {
		$dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone());
		return $dt->getTimestamp();
	}

	/* ---------- Price update stamp ---------- */
	public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void {
		$type = get_post_type($post_id);
		if (!in_array($type, ['product','product_variation'], true)) return;

		if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return;

		update_post_meta($post_id, self::META, time());
	}

	private function last_update_ts($id): int {
		$ts = (int) get_post_meta($id, self::META, true);
		if ($ts > 0) return $ts;

		if ($this->use_fallback()) {
			$post = get_post($id);
			if ($post && !empty($post->post_modified_gmt)) {
				$t = strtotime($post->post_modified_gmt . ' GMT');
				if ($t) return $t;
			}
		}
		return 0;
	}

	private function is_stale($id): bool {
		$ts = $this->last_update_ts($id);
		if ($ts <= 0) return true;
		return $ts < $this->cutoff_ts();
	}

	private function status_payload_for($id): array {
		$stale = $this->is_stale($id);
		return [
			'is_stale' => $stale ? 1 : 0,
			'text'     => $stale ? $this->text_stale() : $this->text_fresh(),
			'class'    => $stale ? 'pcatc-stale' : 'pcatc-fresh',
		];
	}

	/* ---------- WooCommerce blocking ---------- */
	public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) {
		$target_id = $variation_id ? (int)$variation_id : (int)$product_id;

		if ($this->is_stale($target_id)) {
			wc_add_notice($this->get_msg(), 'error');
			return false;
		}
		return $passed;
	}

	public function cart_checkout_notice(): void {
		if (!function_exists('WC') || !WC()->cart) return;

		foreach (WC()->cart->get_cart() as $item) {
			$target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id'];
			if ($this->is_stale($target_id)) {
				wc_print_notice($this->get_msg(), 'error');
				break;
			}
		}
	}

	/* ---------- Front status (simple + variable dynamic) ---------- */
	public function render_front_status_block(): void {
		if (!$this->show_front() || !is_product()) return;

		global $product;
		if (!$product instanceof WC_Product) return;

		// For simple products, render fixed status.
		// For variable products, we render a container that JS will update on variation selection.
		$is_variable = $product->is_type('variable');

		$payload = $this->status_payload_for($product->get_id());
		$text = esc_html($payload['text']);
		$cls  = esc_attr($payload['class']);

		echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">';
		echo $is_variable ? '' : $text;
		echo '</div>';
	}

	public function add_variation_status_data($variation_data, $product, $variation) {
		if (!$this->show_front()) return $variation_data;

		$vid = $variation->get_id();
		$p = $this->status_payload_for($vid);

		$variation_data['pcatc_is_stale'] = $p['is_stale'];
		$variation_data['pcatc_text']     = $p['text'];
		$variation_data['pcatc_class']    = $p['class'];

		return $variation_data;
	}

	public function enqueue_front_js(): void {
		if (!$this->show_front() || !is_product()) return;

		wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true);
		wp_enqueue_script('pcatc-front');

		// Inline CSS (front)
		$css = "
#pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;}
#pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;}
";
		wp_add_inline_style('woocommerce-inline', $css);

		// JS: update status when variation changes
		$js = <<<JS
jQuery(function($){
  var box = $('#pcatc-price-status');
  if(!box.length) return;

  var form = $('form.variations_form');
  if(!form.length) return; // simple product -> no need

  function setStatus(v){
    if(!v || typeof v.pcatc_is_stale === 'undefined'){
      // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده)
      box.text('');
      box.removeClass('pcatc-fresh pcatc-stale');
      return;
    }
    box.text(v.pcatc_text || '');
    box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || '');
  }

  form.on('found_variation', function(e, variation){
    setStatus(variation);
  });

  form.on('reset_data', function(){
    setStatus(null);
  });
});
JS;
		wp_add_inline_script('pcatc-front', $js);
	}

	/* ---------- Admin list column (green/red dot) ---------- */
	public function add_admin_column($columns) {
		if (!$this->show_admin()) return $columns;

		// Insert near price column if possible
		$new = [];
		foreach ($columns as $key => $label) {
			$new[$key] = $label;
			if ($key === 'price') {
				$new['pcatc_status'] = 'وضعیت قیمت';
			}
		}
		if (!isset($new['pcatc_status'])) {
			$new['pcatc_status'] = 'وضعیت قیمت';
		}
		return $new;
	}

	public function render_admin_column($column, $post_id) {
		if (!$this->show_admin()) return;
		if ($column !== 'pcatc_status') return;

		// For variable product: if ANY variation is fresh => green else red
		$product = wc_get_product($post_id);
		if (!$product) return;

		$is_fresh = false;

		if ($product->is_type('variable')) {
			$children = $product->get_children();
			if (!empty($children)) {
				foreach ($children as $vid) {
					if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; }
				}
			}
		} else {
			$is_fresh = !$this->is_stale($post_id);
		}

		echo $is_fresh
			? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>'
			: '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>';
	}

	public function admin_column_css() {
		if (!$this->show_admin()) return;
		echo '<style>
			.column-pcatc_status{width:80px;text-align:center;}
			.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;}
			.pcatc-dot-green{background:#19a64a;}
			.pcatc-dot-red{background:#d10000;}
		</style>';
	}

	/* ---------- Admin settings page ---------- */
	public function add_settings_page(): void {
		add_options_page(
			'تنظیمات قفل خرید بر اساس تاریخ',
			'قفل خرید (تاریخ قیمت)',
			'manage_options',
			'pcatc-settings',
			[$this, 'render_settings_page']
		);
	}

	public function register_settings(): void {
		register_setting('pcatc_settings_group', self::OPT_CUTOFF, [
			'type' => 'string',
			'sanitize_callback' => function($v){
				$v = trim((string)$v);
				return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff();
			}
		]);

		register_setting('pcatc_settings_group', self::OPT_MSG, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_textarea_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_FALLBACK, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);
	}

	public function render_settings_page(): void {
		if (!current_user_can('manage_options')) return;

		$cutoff = esc_attr($this->get_cutoff_date());
		$msg    = esc_textarea($this->get_msg());
		$fb     = $this->use_fallback() ? 'yes' : 'no';

		$sf     = $this->show_front() ? 'yes' : 'no';
		$sa     = $this->show_admin() ? 'yes' : 'no';

		$tf     = esc_attr($this->text_fresh());
		$ts     = esc_attr($this->text_stale());
		?>
		<div class="wrap">
			<h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1>
			<form method="post" action="options.php">
				<?php settings_fields('pcatc_settings_group'); ?>

				<table class="form-table" role="presentation">
					<tr>
						<th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th>
						<td>
							<input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>">
							<p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th>
						<td>
							<textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea>
							<p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th>
						<td>
							<label>
								<input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>>
								اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن.
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row">نمایش وضعیت قیمت</th>
						<td>
							<label style="display:block;margin-bottom:6px;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>>
								نمایش نقطه سبز/قرمز در لیست محصولات (پنل)
							</label>

							<label style="display:block;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>>
								نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای)
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;">
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;">
						</td>
					</tr>
				</table>

				<?php submit_button('ذخیره تنظیمات'); ?>
			</form>
		</div>
		<?php
	}

	/* ---------- Admin bar shortcut ---------- */
	public function admin_bar_link($admin_bar): void {
		if (!is_admin_bar_showing() || !current_user_can('manage_options')) return;
		$admin_bar->add_node([
			'id'    => 'pcatc_settings_link',
			'title' => 'تنظیمات قفل خرید',
			'href'  => admin_url('options-general.php?page=pcatc-settings'),
		]);
	}
}

new PCATC_Settings_Snippet();

کد دوم در قسمت زیر


/* === Front Dot Indicator on Archives (Shop/Category) === */

if (!defined('ABSPATH')) exit;

function pcatc_dot_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01';
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_dot_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_dot_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_dot_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_dot_is_stale($id) {
	$ts = pcatc_dot_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_dot_cutoff_ts();
}

function pcatc_dot_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_dot_is_stale((int)$vid)) return true;
			}
		}
		return false;
	}

	// Simple / others: خود محصول
	return !pcatc_dot_is_stale((int)$product->get_id());
}

/**
 * Add dot next to price on archives (shop/category/tag)
 */
function pcatc_dot_price_html($price_html, $product) {
	if (is_admin()) return $price_html;

	// فقط صفحات لیست محصولات در سایت
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return $price_html;
	}

	// اگر قیمت خالیه، چیزی نزن
	if (trim(wp_strip_all_tags($price_html)) === '') return $price_html;

	$is_fresh = pcatc_dot_product_is_fresh($product);
	$dot = $is_fresh
		? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>'
		: '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>';

	// نقطه + فاصله + قیمت
	return $dot . ' ' . $price_html;
}
add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2);

/** CSS for dots (front) */
function pcatc_dot_css() {
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return;

	echo '<style>
	.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);}
	.pcatc-dot-green{background:#19a64a;}
	.pcatc-dot-red{background:#d10000;}
	</style>';
}
add_action('wp_head', 'pcatc_dot_css', 50);


دسته بندی فقط محصولات قیمت بروز یعنی سبز ها
TEXT - 2026-05-26 18:39:20
/* === Filter product archives by PCATC status: fresh/stale === */ if (!defined('ABSPATH')) exit; function pcatc_filter_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) { $cutoff = '2026-01-01'; } $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_filter_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_filter_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_filter_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_filter_is_stale($id) { $ts = pcatc_filter_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_filter_cutoff_ts(); } function pcatc_filter_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // Variable: اگر حداقل یک variation تازه بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_filter_is_stale((int) $vid)) { return true; } } } return false; } // Simple / others return !pcatc_filter_is_stale((int) $product->get_id()); } add_action('pre_get_posts', function($query) { if (is_admin() || !$query->is_main_query()) return; // فقط در آرشیو محصولات / دسته‌بندی / تگ if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return; } $status = isset($_GET['pcatc_status']) ? sanitize_text_field(wp_unslash($_GET['pcatc_status'])) : ''; if (!in_array($status, ['fresh', 'stale'], true)) return; // همه محصولات صفحه را بگیریم تا بعداً با post__in محدود کنیم $query->set('posts_per_page', -1); add_filter('posts_results', function($posts, $q) use ($status) { if (is_admin() || !$q->is_main_query()) return $posts; if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return $posts; } $filtered_ids = []; foreach ($posts as $post) { $product = wc_get_product($post->ID); if (!$product) continue; $is_fresh = pcatc_filter_product_is_fresh($product); if ($status === 'fresh' && $is_fresh) { $filtered_ids[] = $post->ID; } if ($status === 'stale' && !$is_fresh) { $filtered_ids[] = $post->ID; } } if (empty($filtered_ids)) { $q->set('post__in', [0]); } else { $q->set('post__in', $filtered_ids); } return $posts; }, 10, 2); });
/* === Filter product archives by PCATC status: fresh/stale === */
if (!defined('ABSPATH')) exit;

function pcatc_filter_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) {
		$cutoff = '2026-01-01';
	}
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_filter_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_filter_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_filter_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_filter_is_stale($id) {
	$ts = pcatc_filter_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_filter_cutoff_ts();
}

function pcatc_filter_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// Variable: اگر حداقل یک variation تازه بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_filter_is_stale((int) $vid)) {
					return true;
				}
			}
		}
		return false;
	}

	// Simple / others
	return !pcatc_filter_is_stale((int) $product->get_id());
}

add_action('pre_get_posts', function($query) {
	if (is_admin() || !$query->is_main_query()) return;

	// فقط در آرشیو محصولات / دسته‌بندی / تگ
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return;
	}

	$status = isset($_GET['pcatc_status']) ? sanitize_text_field(wp_unslash($_GET['pcatc_status'])) : '';
	if (!in_array($status, ['fresh', 'stale'], true)) return;

	// همه محصولات صفحه را بگیریم تا بعداً با post__in محدود کنیم
	$query->set('posts_per_page', -1);

	add_filter('posts_results', function($posts, $q) use ($status) {
		if (is_admin() || !$q->is_main_query()) return $posts;

		if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
			return $posts;
		}

		$filtered_ids = [];

		foreach ($posts as $post) {
			$product = wc_get_product($post->ID);
			if (!$product) continue;

			$is_fresh = pcatc_filter_product_is_fresh($product);

			if ($status === 'fresh' && $is_fresh) {
				$filtered_ids[] = $post->ID;
			}

			if ($status === 'stale' && !$is_fresh) {
				$filtered_ids[] = $post->ID;
			}
		}

		if (empty($filtered_ids)) {
			$q->set('post__in', [0]);
		} else {
			$q->set('post__in', $filtered_ids);
		}

		return $posts;
	}, 10, 2);
});
محصولات بروز
TEXT - 2026-05-26 18:36:37
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */ if (!defined('ABSPATH')) exit; class PCATC_Settings_Snippet { // Options const OPT_CUTOFF = 'pcatc_cutoff_date'; const OPT_MSG = 'pcatc_message'; const OPT_FALLBACK = 'pcatc_use_modified_fallback'; const OPT_SHOW_FRONT = 'pcatc_show_front_status'; const OPT_SHOW_ADMIN = 'pcatc_show_admin_status'; const OPT_TEXT_FRESH = 'pcatc_text_fresh'; const OPT_TEXT_STALE = 'pcatc_text_stale'; // Meta const META = '_pcatc_price_last_updated'; public function __construct() { // Admin settings UI add_action('admin_menu', [$this, 'add_settings_page']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100); // Stamp when price changes add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); add_action('added_post_meta', [$this,'maybe_stamp_price_update'], 10, 4); // Block add to cart + notices add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5); add_action('woocommerce_before_cart', [$this,'cart_checkout_notice']); add_action('woocommerce_before_checkout_form', [$this,'cart_checkout_notice']); // Front indicators add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11); add_filter('woocommerce_available_variation', [$this,'add_variation_status_data'], 10, 3); add_action('wp_enqueue_scripts', [$this,'enqueue_front_js']); // Admin list indicator add_filter('manage_edit-product_columns', [$this,'add_admin_column'], 30); add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2); add_action('admin_head', [$this,'admin_column_css']); } /* ---------- Defaults ---------- */ private function default_cutoff(): string { return '2026-01-01'; } private function default_msg(): string { return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.'; } private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; } private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; } /* ---------- Options getters ---------- */ private function get_cutoff_date(): string { $val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff()); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff(); return $val; } private function get_msg(): string { $val = (string) get_option(self::OPT_MSG, $this->default_msg()); return $val !== '' ? $val : $this->default_msg(); } private function use_fallback(): bool { return get_option(self::OPT_FALLBACK, 'yes') === 'yes'; } private function show_front(): bool { return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes'; } private function show_admin(): bool { return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes'; } private function text_fresh(): string { $val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh()); return $val !== '' ? $val : $this->default_text_fresh(); } private function text_stale(): string { $val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale()); return $val !== '' ? $val : $this->default_text_stale(); } /* ---------- Cutoff timestamp ---------- */ private function cutoff_ts(): int { $dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } /* ---------- Price update stamp ---------- */ public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void { $type = get_post_type($post_id); if (!in_array($type, ['product','product_variation'], true)) return; if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return; update_post_meta($post_id, self::META, time()); } private function last_update_ts($id): int { $ts = (int) get_post_meta($id, self::META, true); if ($ts > 0) return $ts; if ($this->use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } private function is_stale($id): bool { $ts = $this->last_update_ts($id); if ($ts <= 0) return true; return $ts < $this->cutoff_ts(); } private function status_payload_for($id): array { $stale = $this->is_stale($id); return [ 'is_stale' => $stale ? 1 : 0, 'text' => $stale ? $this->text_stale() : $this->text_fresh(), 'class' => $stale ? 'pcatc-stale' : 'pcatc-fresh', ]; } /* ---------- WooCommerce blocking ---------- */ public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) { $target_id = $variation_id ? (int)$variation_id : (int)$product_id; if ($this->is_stale($target_id)) { wc_add_notice($this->get_msg(), 'error'); return false; } return $passed; } public function cart_checkout_notice(): void { if (!function_exists('WC') || !WC()->cart) return; foreach (WC()->cart->get_cart() as $item) { $target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id']; if ($this->is_stale($target_id)) { wc_print_notice($this->get_msg(), 'error'); break; } } } /* ---------- Front status (simple + variable dynamic) ---------- */ public function render_front_status_block(): void { if (!$this->show_front() || !is_product()) return; global $product; if (!$product instanceof WC_Product) return; // For simple products, render fixed status. // For variable products, we render a container that JS will update on variation selection. $is_variable = $product->is_type('variable'); $payload = $this->status_payload_for($product->get_id()); $text = esc_html($payload['text']); $cls = esc_attr($payload['class']); echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">'; echo $is_variable ? '' : $text; echo '</div>'; } public function add_variation_status_data($variation_data, $product, $variation) { if (!$this->show_front()) return $variation_data; $vid = $variation->get_id(); $p = $this->status_payload_for($vid); $variation_data['pcatc_is_stale'] = $p['is_stale']; $variation_data['pcatc_text'] = $p['text']; $variation_data['pcatc_class'] = $p['class']; return $variation_data; } public function enqueue_front_js(): void { if (!$this->show_front() || !is_product()) return; wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true); wp_enqueue_script('pcatc-front'); // Inline CSS (front) $css = " #pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;} #pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;} "; wp_add_inline_style('woocommerce-inline', $css); // JS: update status when variation changes $js = <<<JS jQuery(function($){ var box = $('#pcatc-price-status'); if(!box.length) return; var form = $('form.variations_form'); if(!form.length) return; // simple product -> no need function setStatus(v){ if(!v || typeof v.pcatc_is_stale === 'undefined'){ // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده) box.text(''); box.removeClass('pcatc-fresh pcatc-stale'); return; } box.text(v.pcatc_text || ''); box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || ''); } form.on('found_variation', function(e, variation){ setStatus(variation); }); form.on('reset_data', function(){ setStatus(null); }); }); JS; wp_add_inline_script('pcatc-front', $js); } /* ---------- Admin list column (green/red dot) ---------- */ public function add_admin_column($columns) { if (!$this->show_admin()) return $columns; // Insert near price column if possible $new = []; foreach ($columns as $key => $label) { $new[$key] = $label; if ($key === 'price') { $new['pcatc_status'] = 'وضعیت قیمت'; } } if (!isset($new['pcatc_status'])) { $new['pcatc_status'] = 'وضعیت قیمت'; } return $new; } public function render_admin_column($column, $post_id) { if (!$this->show_admin()) return; if ($column !== 'pcatc_status') return; // For variable product: if ANY variation is fresh => green else red $product = wc_get_product($post_id); if (!$product) return; $is_fresh = false; if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; } } } } else { $is_fresh = !$this->is_stale($post_id); } echo $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>'; } public function admin_column_css() { if (!$this->show_admin()) return; echo '<style> .column-pcatc_status{width:80px;text-align:center;} .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } /* ---------- Admin settings page ---------- */ public function add_settings_page(): void { add_options_page( 'تنظیمات قفل خرید بر اساس تاریخ', 'قفل خرید (تاریخ قیمت)', 'manage_options', 'pcatc-settings', [$this, 'render_settings_page'] ); } public function register_settings(): void { register_setting('pcatc_settings_group', self::OPT_CUTOFF, [ 'type' => 'string', 'sanitize_callback' => function($v){ $v = trim((string)$v); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff(); } ]); register_setting('pcatc_settings_group', self::OPT_MSG, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_textarea_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_FALLBACK, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [ 'type' => 'string', 'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [ 'type' => 'string', 'sanitize_callback' => function($v){ return sanitize_text_field($v); } ]); } public function render_settings_page(): void { if (!current_user_can('manage_options')) return; $cutoff = esc_attr($this->get_cutoff_date()); $msg = esc_textarea($this->get_msg()); $fb = $this->use_fallback() ? 'yes' : 'no'; $sf = $this->show_front() ? 'yes' : 'no'; $sa = $this->show_admin() ? 'yes' : 'no'; $tf = esc_attr($this->text_fresh()); $ts = esc_attr($this->text_stale()); ?> <div class="wrap"> <h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1> <form method="post" action="options.php"> <?php settings_fields('pcatc_settings_group'); ?> <table class="form-table" role="presentation"> <tr> <th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th> <td> <input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>"> <p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th> <td> <textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea> <p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p> </td> </tr> <tr> <th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th> <td> <label> <input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>> اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن. </label> </td> </tr> <tr> <th scope="row">نمایش وضعیت قیمت</th> <td> <label style="display:block;margin-bottom:6px;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>> نمایش نقطه سبز/قرمز در لیست محصولات (پنل) </label> <label style="display:block;"> <input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>> نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای) </label> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;"> </td> </tr> <tr> <th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th> <td> <input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;"> </td> </tr> </table> <?php submit_button('ذخیره تنظیمات'); ?> </form> </div> <?php } /* ---------- Admin bar shortcut ---------- */ public function admin_bar_link($admin_bar): void { if (!is_admin_bar_showing() || !current_user_can('manage_options')) return; $admin_bar->add_node([ 'id' => 'pcatc_settings_link', 'title' => 'تنظیمات قفل خرید', 'href' => admin_url('options-general.php?page=pcatc-settings'), ]); } } new PCATC_Settings_Snippet(); کد دوم در قسمت زیر /* === Front Dot Indicator on Archives (Shop/Category) === */ if (!defined('ABSPATH')) exit; function pcatc_dot_cutoff_ts() { $cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01'; $dt = new DateTime($cutoff . ' 00:00:00', wp_timezone()); return $dt->getTimestamp(); } function pcatc_dot_use_fallback() { return get_option('pcatc_use_modified_fallback', 'yes') === 'yes'; } function pcatc_dot_last_update_ts($id) { $ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true); if ($ts > 0) return $ts; if (pcatc_dot_use_fallback()) { $post = get_post($id); if ($post && !empty($post->post_modified_gmt)) { $t = strtotime($post->post_modified_gmt . ' GMT'); if ($t) return $t; } } return 0; } function pcatc_dot_is_stale($id) { $ts = pcatc_dot_last_update_ts($id); if ($ts <= 0) return true; return $ts < pcatc_dot_cutoff_ts(); } function pcatc_dot_product_is_fresh($product) { if (!$product instanceof WC_Product) return false; // Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز if ($product->is_type('variable')) { $children = $product->get_children(); if (!empty($children)) { foreach ($children as $vid) { if (!pcatc_dot_is_stale((int)$vid)) return true; } } return false; } // Simple / others: خود محصول return !pcatc_dot_is_stale((int)$product->get_id()); } /** * Add dot next to price on archives (shop/category/tag) */ function pcatc_dot_price_html($price_html, $product) { if (is_admin()) return $price_html; // فقط صفحات لیست محصولات در سایت if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) { return $price_html; } // اگر قیمت خالیه، چیزی نزن if (trim(wp_strip_all_tags($price_html)) === '') return $price_html; $is_fresh = pcatc_dot_product_is_fresh($product); $dot = $is_fresh ? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>' : '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>'; // نقطه + فاصله + قیمت return $dot . ' ' . $price_html; } add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2); /** CSS for dots (front) */ function pcatc_dot_css() { if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return; echo '<style> .pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);} .pcatc-dot-green{background:#19a64a;} .pcatc-dot-red{background:#d10000;} </style>'; } add_action('wp_head', 'pcatc_dot_css', 50);
/* === Price Cutoff: Disable Add to Cart + Settings + Indicators (Simple + Variations) === */

if (!defined('ABSPATH')) exit;

class PCATC_Settings_Snippet {
	// Options
	const OPT_CUTOFF         = 'pcatc_cutoff_date';
	const OPT_MSG            = 'pcatc_message';
	const OPT_FALLBACK       = 'pcatc_use_modified_fallback';

	const OPT_SHOW_FRONT     = 'pcatc_show_front_status';
	const OPT_SHOW_ADMIN     = 'pcatc_show_admin_status';
	const OPT_TEXT_FRESH     = 'pcatc_text_fresh';
	const OPT_TEXT_STALE     = 'pcatc_text_stale';

	// Meta
	const META               = '_pcatc_price_last_updated';

	public function __construct() {
		// Admin settings UI
		add_action('admin_menu', [$this, 'add_settings_page']);
		add_action('admin_init', [$this, 'register_settings']);
		add_action('admin_bar_menu', [$this, 'admin_bar_link'], 100);

		// Stamp when price changes
		add_action('updated_post_meta', [$this,'maybe_stamp_price_update'], 10, 4);
		add_action('added_post_meta',   [$this,'maybe_stamp_price_update'], 10, 4);

		// Block add to cart + notices
		add_filter('woocommerce_add_to_cart_validation', [$this,'block_add_to_cart'], 10, 5);
		add_action('woocommerce_before_cart',            [$this,'cart_checkout_notice']);
		add_action('woocommerce_before_checkout_form',   [$this,'cart_checkout_notice']);

		// Front indicators
		add_action('woocommerce_single_product_summary', [$this,'render_front_status_block'], 11);
		add_filter('woocommerce_available_variation',    [$this,'add_variation_status_data'], 10, 3);
		add_action('wp_enqueue_scripts',                 [$this,'enqueue_front_js']);

		// Admin list indicator
		add_filter('manage_edit-product_columns',        [$this,'add_admin_column'], 30);
		add_action('manage_product_posts_custom_column', [$this,'render_admin_column'], 30, 2);
		add_action('admin_head',                         [$this,'admin_column_css']);
	}

	/* ---------- Defaults ---------- */
	private function default_cutoff(): string { return '2026-01-01'; }
	private function default_msg(): string {
		return 'قیمت این محصول به‌روز نیست. برای خرید با پشتیبانی تماس بگیرید.';
	}
	private function default_text_fresh(): string { return 'قیمت به‌روز است ✅'; }
	private function default_text_stale(): string { return 'قیمت به‌روز نیست ❌'; }

	/* ---------- Options getters ---------- */
	private function get_cutoff_date(): string {
		$val = (string) get_option(self::OPT_CUTOFF, $this->default_cutoff());
		if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) return $this->default_cutoff();
		return $val;
	}
	private function get_msg(): string {
		$val = (string) get_option(self::OPT_MSG, $this->default_msg());
		return $val !== '' ? $val : $this->default_msg();
	}
	private function use_fallback(): bool {
		return get_option(self::OPT_FALLBACK, 'yes') === 'yes';
	}
	private function show_front(): bool {
		return get_option(self::OPT_SHOW_FRONT, 'yes') === 'yes';
	}
	private function show_admin(): bool {
		return get_option(self::OPT_SHOW_ADMIN, 'yes') === 'yes';
	}
	private function text_fresh(): string {
		$val = (string) get_option(self::OPT_TEXT_FRESH, $this->default_text_fresh());
		return $val !== '' ? $val : $this->default_text_fresh();
	}
	private function text_stale(): string {
		$val = (string) get_option(self::OPT_TEXT_STALE, $this->default_text_stale());
		return $val !== '' ? $val : $this->default_text_stale();
	}

	/* ---------- Cutoff timestamp ---------- */
	private function cutoff_ts(): int {
		$dt = new DateTime($this->get_cutoff_date() . ' 00:00:00', wp_timezone());
		return $dt->getTimestamp();
	}

	/* ---------- Price update stamp ---------- */
	public function maybe_stamp_price_update($meta_id, $post_id, $meta_key, $meta_value): void {
		$type = get_post_type($post_id);
		if (!in_array($type, ['product','product_variation'], true)) return;

		if (!in_array($meta_key, ['_price','_regular_price','_sale_price'], true)) return;

		update_post_meta($post_id, self::META, time());
	}

	private function last_update_ts($id): int {
		$ts = (int) get_post_meta($id, self::META, true);
		if ($ts > 0) return $ts;

		if ($this->use_fallback()) {
			$post = get_post($id);
			if ($post && !empty($post->post_modified_gmt)) {
				$t = strtotime($post->post_modified_gmt . ' GMT');
				if ($t) return $t;
			}
		}
		return 0;
	}

	private function is_stale($id): bool {
		$ts = $this->last_update_ts($id);
		if ($ts <= 0) return true;
		return $ts < $this->cutoff_ts();
	}

	private function status_payload_for($id): array {
		$stale = $this->is_stale($id);
		return [
			'is_stale' => $stale ? 1 : 0,
			'text'     => $stale ? $this->text_stale() : $this->text_fresh(),
			'class'    => $stale ? 'pcatc-stale' : 'pcatc-fresh',
		];
	}

	/* ---------- WooCommerce blocking ---------- */
	public function block_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) {
		$target_id = $variation_id ? (int)$variation_id : (int)$product_id;

		if ($this->is_stale($target_id)) {
			wc_add_notice($this->get_msg(), 'error');
			return false;
		}
		return $passed;
	}

	public function cart_checkout_notice(): void {
		if (!function_exists('WC') || !WC()->cart) return;

		foreach (WC()->cart->get_cart() as $item) {
			$target_id = !empty($item['variation_id']) ? (int)$item['variation_id'] : (int)$item['product_id'];
			if ($this->is_stale($target_id)) {
				wc_print_notice($this->get_msg(), 'error');
				break;
			}
		}
	}

	/* ---------- Front status (simple + variable dynamic) ---------- */
	public function render_front_status_block(): void {
		if (!$this->show_front() || !is_product()) return;

		global $product;
		if (!$product instanceof WC_Product) return;

		// For simple products, render fixed status.
		// For variable products, we render a container that JS will update on variation selection.
		$is_variable = $product->is_type('variable');

		$payload = $this->status_payload_for($product->get_id());
		$text = esc_html($payload['text']);
		$cls  = esc_attr($payload['class']);

		echo '<div id="pcatc-price-status" class="pcatc-price-status '. $cls .'" style="margin-top:6px;font-size:14px;line-height:1.4;">';
		echo $is_variable ? '' : $text;
		echo '</div>';
	}

	public function add_variation_status_data($variation_data, $product, $variation) {
		if (!$this->show_front()) return $variation_data;

		$vid = $variation->get_id();
		$p = $this->status_payload_for($vid);

		$variation_data['pcatc_is_stale'] = $p['is_stale'];
		$variation_data['pcatc_text']     = $p['text'];
		$variation_data['pcatc_class']    = $p['class'];

		return $variation_data;
	}

	public function enqueue_front_js(): void {
		if (!$this->show_front() || !is_product()) return;

		wp_register_script('pcatc-front', '', ['jquery'], '1.0.0', true);
		wp_enqueue_script('pcatc-front');

		// Inline CSS (front)
		$css = "
#pcatc-price-status.pcatc-fresh{color:#0a8a2a;font-weight:600;}
#pcatc-price-status.pcatc-stale{color:#c40000;font-weight:600;}
";
		wp_add_inline_style('woocommerce-inline', $css);

		// JS: update status when variation changes
		$js = <<<JS
jQuery(function($){
  var box = $('#pcatc-price-status');
  if(!box.length) return;

  var form = $('form.variations_form');
  if(!form.length) return; // simple product -> no need

  function setStatus(v){
    if(!v || typeof v.pcatc_is_stale === 'undefined'){
      // اگر ورییشن انتخاب نشد، پیام را خالی بگذار (یا اگر خواستی یک متن پیش‌فرض بده)
      box.text('');
      box.removeClass('pcatc-fresh pcatc-stale');
      return;
    }
    box.text(v.pcatc_text || '');
    box.removeClass('pcatc-fresh pcatc-stale').addClass(v.pcatc_class || '');
  }

  form.on('found_variation', function(e, variation){
    setStatus(variation);
  });

  form.on('reset_data', function(){
    setStatus(null);
  });
});
JS;
		wp_add_inline_script('pcatc-front', $js);
	}

	/* ---------- Admin list column (green/red dot) ---------- */
	public function add_admin_column($columns) {
		if (!$this->show_admin()) return $columns;

		// Insert near price column if possible
		$new = [];
		foreach ($columns as $key => $label) {
			$new[$key] = $label;
			if ($key === 'price') {
				$new['pcatc_status'] = 'وضعیت قیمت';
			}
		}
		if (!isset($new['pcatc_status'])) {
			$new['pcatc_status'] = 'وضعیت قیمت';
		}
		return $new;
	}

	public function render_admin_column($column, $post_id) {
		if (!$this->show_admin()) return;
		if ($column !== 'pcatc_status') return;

		// For variable product: if ANY variation is fresh => green else red
		$product = wc_get_product($post_id);
		if (!$product) return;

		$is_fresh = false;

		if ($product->is_type('variable')) {
			$children = $product->get_children();
			if (!empty($children)) {
				foreach ($children as $vid) {
					if (!$this->is_stale((int)$vid)) { $is_fresh = true; break; }
				}
			}
		} else {
			$is_fresh = !$this->is_stale($post_id);
		}

		echo $is_fresh
			? '<span class="pcatc-dot pcatc-dot-green" title="به‌روز"></span>'
			: '<span class="pcatc-dot pcatc-dot-red" title="قدیمی"></span>';
	}

	public function admin_column_css() {
		if (!$this->show_admin()) return;
		echo '<style>
			.column-pcatc_status{width:80px;text-align:center;}
			.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;}
			.pcatc-dot-green{background:#19a64a;}
			.pcatc-dot-red{background:#d10000;}
		</style>';
	}

	/* ---------- Admin settings page ---------- */
	public function add_settings_page(): void {
		add_options_page(
			'تنظیمات قفل خرید بر اساس تاریخ',
			'قفل خرید (تاریخ قیمت)',
			'manage_options',
			'pcatc-settings',
			[$this, 'render_settings_page']
		);
	}

	public function register_settings(): void {
		register_setting('pcatc_settings_group', self::OPT_CUTOFF, [
			'type' => 'string',
			'sanitize_callback' => function($v){
				$v = trim((string)$v);
				return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $this->default_cutoff();
			}
		]);

		register_setting('pcatc_settings_group', self::OPT_MSG, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_textarea_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_FALLBACK, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_FRONT, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_SHOW_ADMIN, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return ($v === 'yes') ? 'yes' : 'no'; }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_FRESH, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);

		register_setting('pcatc_settings_group', self::OPT_TEXT_STALE, [
			'type' => 'string',
			'sanitize_callback' => function($v){ return sanitize_text_field($v); }
		]);
	}

	public function render_settings_page(): void {
		if (!current_user_can('manage_options')) return;

		$cutoff = esc_attr($this->get_cutoff_date());
		$msg    = esc_textarea($this->get_msg());
		$fb     = $this->use_fallback() ? 'yes' : 'no';

		$sf     = $this->show_front() ? 'yes' : 'no';
		$sa     = $this->show_admin() ? 'yes' : 'no';

		$tf     = esc_attr($this->text_fresh());
		$ts     = esc_attr($this->text_stale());
		?>
		<div class="wrap">
			<h1>تنظیمات قفل خرید بر اساس تاریخ قیمت</h1>
			<form method="post" action="options.php">
				<?php settings_fields('pcatc_settings_group'); ?>

				<table class="form-table" role="presentation">
					<tr>
						<th scope="row"><label for="<?php echo self::OPT_CUTOFF; ?>">تاریخ کات‌آف</label></th>
						<td>
							<input type="date" id="<?php echo self::OPT_CUTOFF; ?>" name="<?php echo self::OPT_CUTOFF; ?>" value="<?php echo $cutoff; ?>">
							<p class="description">اگر آخرین آپدیت قیمت قبل از این تاریخ باشد، افزودن به سبد غیرفعال می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_MSG; ?>">متن پیام (برای بلاک خرید)</label></th>
						<td>
							<textarea id="<?php echo self::OPT_MSG; ?>" name="<?php echo self::OPT_MSG; ?>" rows="3" cols="60"><?php echo $msg; ?></textarea>
							<p class="description">این پیام هنگام تلاش برای افزودن به سبد و همچنین در سبد/تسویه‌حساب نمایش داده می‌شود.</p>
						</td>
					</tr>

					<tr>
						<th scope="row">Fallback (هماهنگ با افزونه نمایش تاریخ)</th>
						<td>
							<label>
								<input type="checkbox" name="<?php echo self::OPT_FALLBACK; ?>" value="yes" <?php checked('yes', $fb); ?>>
								اگر تاریخ «آخرین آپدیت قیمت» ثبت نشده بود، از «آخرین ویرایش محصول» استفاده کن.
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row">نمایش وضعیت قیمت</th>
						<td>
							<label style="display:block;margin-bottom:6px;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_ADMIN; ?>" value="yes" <?php checked('yes', $sa); ?>>
								نمایش نقطه سبز/قرمز در لیست محصولات (پنل)
							</label>

							<label style="display:block;">
								<input type="checkbox" name="<?php echo self::OPT_SHOW_FRONT; ?>" value="yes" <?php checked('yes', $sf); ?>>
								نمایش متن سبز/قرمز کنار قیمت در صفحه محصول (حتی برای تنوع‌ها به‌صورت لحظه‌ای)
							</label>
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_FRESH; ?>">متن وضعیت سبز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_FRESH; ?>" name="<?php echo self::OPT_TEXT_FRESH; ?>" value="<?php echo $tf; ?>" style="width:420px;">
						</td>
					</tr>

					<tr>
						<th scope="row"><label for="<?php echo self::OPT_TEXT_STALE; ?>">متن وضعیت قرمز</label></th>
						<td>
							<input type="text" id="<?php echo self::OPT_TEXT_STALE; ?>" name="<?php echo self::OPT_TEXT_STALE; ?>" value="<?php echo $ts; ?>" style="width:420px;">
						</td>
					</tr>
				</table>

				<?php submit_button('ذخیره تنظیمات'); ?>
			</form>
		</div>
		<?php
	}

	/* ---------- Admin bar shortcut ---------- */
	public function admin_bar_link($admin_bar): void {
		if (!is_admin_bar_showing() || !current_user_can('manage_options')) return;
		$admin_bar->add_node([
			'id'    => 'pcatc_settings_link',
			'title' => 'تنظیمات قفل خرید',
			'href'  => admin_url('options-general.php?page=pcatc-settings'),
		]);
	}
}

new PCATC_Settings_Snippet();

کد دوم در قسمت زیر


/* === Front Dot Indicator on Archives (Shop/Category) === */

if (!defined('ABSPATH')) exit;

function pcatc_dot_cutoff_ts() {
	$cutoff = (string) get_option('pcatc_cutoff_date', '2026-01-01');
	if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $cutoff)) $cutoff = '2026-01-01';
	$dt = new DateTime($cutoff . ' 00:00:00', wp_timezone());
	return $dt->getTimestamp();
}

function pcatc_dot_use_fallback() {
	return get_option('pcatc_use_modified_fallback', 'yes') === 'yes';
}

function pcatc_dot_last_update_ts($id) {
	$ts = (int) get_post_meta($id, '_pcatc_price_last_updated', true);
	if ($ts > 0) return $ts;

	if (pcatc_dot_use_fallback()) {
		$post = get_post($id);
		if ($post && !empty($post->post_modified_gmt)) {
			$t = strtotime($post->post_modified_gmt . ' GMT');
			if ($t) return $t;
		}
	}
	return 0;
}

function pcatc_dot_is_stale($id) {
	$ts = pcatc_dot_last_update_ts($id);
	if ($ts <= 0) return true;
	return $ts < pcatc_dot_cutoff_ts();
}

function pcatc_dot_product_is_fresh($product) {
	if (!$product instanceof WC_Product) return false;

	// Variable: اگر حتی یکی از تنوع‌ها به‌روز بود => سبز
	if ($product->is_type('variable')) {
		$children = $product->get_children();
		if (!empty($children)) {
			foreach ($children as $vid) {
				if (!pcatc_dot_is_stale((int)$vid)) return true;
			}
		}
		return false;
	}

	// Simple / others: خود محصول
	return !pcatc_dot_is_stale((int)$product->get_id());
}

/**
 * Add dot next to price on archives (shop/category/tag)
 */
function pcatc_dot_price_html($price_html, $product) {
	if (is_admin()) return $price_html;

	// فقط صفحات لیست محصولات در سایت
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) {
		return $price_html;
	}

	// اگر قیمت خالیه، چیزی نزن
	if (trim(wp_strip_all_tags($price_html)) === '') return $price_html;

	$is_fresh = pcatc_dot_product_is_fresh($product);
	$dot = $is_fresh
		? '<span class="pcatc-dot pcatc-dot-green" aria-label="قیمت به‌روز"></span>'
		: '<span class="pcatc-dot pcatc-dot-red" aria-label="قیمت به‌روز نیست"></span>';

	// نقطه + فاصله + قیمت
	return $dot . ' ' . $price_html;
}
add_filter('woocommerce_get_price_html', 'pcatc_dot_price_html', 20, 2);

/** CSS for dots (front) */
function pcatc_dot_css() {
	if (!(is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy())) return;

	echo '<style>
	.pcatc-dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-left:6px;transform:translateY(-1px);}
	.pcatc-dot-green{background:#19a64a;}
	.pcatc-dot-red{background:#d10000;}
	</style>';
}
add_action('wp_head', 'pcatc_dot_css', 50);


تغییر قیمت
TEXT - 2026-05-18 10:05:42
/** * Front-end Price Editor (Simple + Variable) - Code Snippets * ✅ فقط قیمت عادی (Regular) — بدون فروش ویژه * ✅ نمایش باکس جمع/بازشو + ریسپانسیو موبایل + رنگ‌بندی اکسل * ✅ فرمت سه‌تایی حین تایپ (23,000,000) ولی ذخیره امن (فقط ارقام) * ✅ فقط همان تنوعی که واقعاً تغییر کرده save می‌شود (تاریخ بقیه تنوع‌ها آپدیت نمی‌شود) * ✅ دکمه مثل وردپرس: «به‌روزرسانی» * * نصب: Code Snippets → Add New → PHP → Run everywhere → فعال */ if ( ! defined('ABSPATH') ) exit; /** Optional: remove "choose an option" placeholder in variation dropdowns */ add_filter('woocommerce_dropdown_variation_attribute_options_args', function($args){ $args['show_option_none'] = false; return $args; }); /** Convert Persian/Arabic digits to English + keep only digits */ function fpe_digits_only($val){ $val = (string) $val; $map = [ '۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9', '٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9', ]; $val = strtr($val, $map); return preg_replace('/\D+/', '', $val); } /** Variation label helpers */ function fpe_attribute_label($attr_key, $parent_product){ $key = preg_replace('/^attribute_/', '', (string)$attr_key); if (strpos($key, 'pa_') === 0 && taxonomy_exists($key)) { $tax = get_taxonomy($key); if ($tax && ! empty($tax->labels->singular_name)) return $tax->labels->singular_name; return wc_attribute_label($key, $parent_product); } $label = wc_attribute_label($key, $parent_product); if ($label && $label !== $key) return $label; return str_replace(['pa_', '-', '_'], ['', ' ', ' '], $key); } function fpe_attribute_value_readable($taxonomy_or_name, $raw_val){ $raw_val = (string)$raw_val; $decoded = rawurldecode($raw_val); $tax = preg_replace('/^attribute_/', '', (string)$taxonomy_or_name); if (strpos($tax, 'pa_') === 0 && taxonomy_exists($tax)) { $term = get_term_by('slug', $raw_val, $tax); if ( ! $term || is_wp_error($term) ) $term = get_term_by('slug', $decoded, $tax); if ( ! $term || is_wp_error($term) ) $term = get_term_by('name', $decoded, $tax); if ( $term && ! is_wp_error($term) ) return $term->name; return $decoded; } return $decoded; } function fpe_get_variation_label($variation, $parent_product){ $out = []; foreach ((array)$variation->get_attributes() as $k => $v) { if ($v === '' || $v === null) continue; $out[] = fpe_attribute_label($k, $parent_product) . ': ' . fpe_attribute_value_readable($k, $v); } return $out ? implode(' | ', $out) : ('تنوع #' . $variation->get_id()); } /** UI */ add_action('woocommerce_after_add_to_cart_form', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( ! is_user_logged_in() ) return; if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) return; global $product; if ( ! $product ) return; $type = $product->get_type(); if ( $type !== 'simple' && $type !== 'variable' ) return; $product_id = $product->get_id(); $nonce = wp_create_nonce('fpe_save_prices'); $title = ($type === 'simple') ? 'ویرایش قیمت محصول (ساده)' : 'ویرایش قیمت تنوع‌ها (متغیر)'; echo '<style> .fpe-wrap{margin:16px 0;} .fpe-details{border:1px solid #e5e7eb;border-radius:14px;background:#fafafa;overflow:hidden;} .fpe-details>summary{list-style:none;cursor:pointer;padding:12px;display:flex;align-items:center;gap:10px;user-select:none;} .fpe-details>summary::-webkit-details-marker{display:none;} .fpe-badge{font-size:12px;padding:4px 10px;border-radius:999px;background:#111;color:#fff;white-space:nowrap;} .fpe-title{font-size:14px;font-weight:900;line-height:1.4;margin:0;flex:1;} .fpe-hint{font-size:12px;opacity:.7;margin:0;} .fpe-body{padding:12px;} .fpe-grid-head{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:10px;border-bottom:1px solid #e5e7eb;font-size:13px;font-weight:900;background:#f3f4f6;border-radius:12px;margin-bottom:10px;} .fpe-row{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:12px 10px;border:1px solid #e5e7eb;border-radius:14px;align-items:center;margin-bottom:10px;background:#fff;} .fpe-rows .fpe-row:nth-child(even){ background:#eef6ff; } .fpe-attr{font-size:13px;line-height:1.6;word-break:break-word;font-weight:800;} .fpe-input{ width:100%;padding:10px;border:1px solid #c3c4c7;border-radius:10px;font-size:16px;outline:none;background:#fff; direction:ltr;text-align:center; } .fpe-input:focus{border-color:#2271b1; box-shadow:0 0 0 1px #2271b1;} .fpe-actions{margin-top:12px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;} .fpe-note{font-size:12px;opacity:.75;margin:0;} .fpe-btn{width:100%;padding:12px 18px;border:1px solid #2271b1;border-radius:6px;cursor:pointer;font-size:14px;font-weight:700;background:#2271b1;color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.08);} .fpe-btn:hover{background:#135e96;border-color:#135e96;} .fpe-btn:active{background:#0a4b78;border-color:#0a4b78;transform:translateY(1px);} @media (max-width:680px){ .fpe-grid-head{display:none;} .fpe-row{grid-template-columns:1fr;gap:10px;padding:12px;} .fpe-field{display:flex;flex-direction:column;gap:6px;} .fpe-label{font-size:12px;opacity:.75;} .fpe-attr{font-size:14px;} } @media (min-width:681px){ .fpe-btn{width:auto;min-width:220px;} .fpe-label{display:none;} .fpe-field{display:block;} } </style>'; echo '<div class="fpe-wrap">'; echo '<details class="fpe-details" '.(isset($_GET["fpe_saved"]) ? "open" : "").'>'; echo '<summary><span class="fpe-badge">مدیر</span> <div style="min-width:0;"> <p class="fpe-title">'.esc_html($title).'</p> <p class="fpe-hint">قیمت‌ها حین تایپ سه‌تایی جدا می‌شوند ✅</p> </div> <span style="opacity:.65;font-size:18px;">⌄</span> </summary>'; echo '<div class="fpe-body"><form method="post" id="fpe-form">'; echo '<input type="hidden" name="fpe_product_id" value="'.esc_attr($product_id).'">'; echo '<input type="hidden" name="fpe_nonce" value="'.esc_attr($nonce).'">'; echo '<input type="hidden" name="fpe_type" value="'.esc_attr($type).'">'; echo '<div class="fpe-grid-head"><div>'.($type==='simple'?'محصول':'تنوع').'</div><div>قیمت</div></div>'; echo '<div class="fpe-rows">'; if ($type === 'simple') { $raw = fpe_digits_only($product->get_regular_price()); echo '<div class="fpe-row"> <div class="fpe-attr">این محصول</div> <div class="fpe-field"> <div class="fpe-label">قیمت</div> <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_simple_regular" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000"> </div> </div>'; } else { foreach ($product->get_children() as $variation_id) { $v = wc_get_product($variation_id); if (!$v) continue; $label = fpe_get_variation_label($v, $product); $raw = fpe_digits_only($v->get_regular_price()); echo '<div class="fpe-row"> <div class="fpe-attr">'.esc_html($label).'</div> <div class="fpe-field"> <div class="fpe-label">قیمت</div> <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_regular['.esc_attr($variation_id).']" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000"> </div> </div>'; } } echo '</div>'; echo '<div class="fpe-actions"> <button class="fpe-btn" type="submit" name="fpe_save" value="1">به‌روزرسانی</button> <p class="fpe-note">بعد از به‌روزرسانی، صفحه رفرش می‌شود.</p> </div>'; echo '</form></div></details></div>'; // JS: format while typing + keep caret position + submit digits-only echo '<script> (function(){ function toEnDigits(s){ if(!s) return ""; var map = {"۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"}; return String(s).replace(/[۰-۹٠-٩]/g, function(ch){ return map[ch] || ch; }); } function digitsOnly(s){ return toEnDigits(s).replace(/\\D+/g,""); } function format3(s){ s = digitsOnly(s); if(!s) return ""; return s.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ","); } function caretDigitsIndex(value, caretPos){ var left = value.slice(0, caretPos); return digitsOnly(left).length; } function caretFromDigitsIndex(formatted, digitIndex){ var count = 0; for (var i=0; i<formatted.length; i++){ if (/\\d/.test(formatted[i])) count++; if (count >= digitIndex) return i+1; } return formatted.length; } var inputs = document.querySelectorAll(".fpe-wrap .fpe-price"); inputs.forEach(function(inp){ inp.value = format3(inp.value); inp.addEventListener("input", function(){ var oldVal = inp.value; var caret = inp.selectionStart || 0; var dIndex = caretDigitsIndex(oldVal, caret); var newVal = format3(oldVal); inp.value = newVal; var newCaret = caretFromDigitsIndex(newVal, dIndex); try { inp.setSelectionRange(newCaret, newCaret); } catch(err){} }); inp.addEventListener("paste", function(){ setTimeout(function(){ inp.value = format3(inp.value); }, 0); }); }); var form = document.getElementById("fpe-form"); if(form){ form.addEventListener("submit", function(){ inputs.forEach(function(inp){ inp.value = digitsOnly(inp.value); }); }); } })(); </script>'; }, 50); /** Save handler (ONLY regular price) */ add_action('template_redirect', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( empty($_POST['fpe_save']) ) return; if ( ! is_user_logged_in() ) wp_die('Access denied'); if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) wp_die('Access denied'); $nonce = isset($_POST['fpe_nonce']) ? sanitize_text_field($_POST['fpe_nonce']) : ''; if ( ! wp_verify_nonce($nonce, 'fpe_save_prices') ) wp_die('Security check failed'); $product_id = isset($_POST['fpe_product_id']) ? absint($_POST['fpe_product_id']) : 0; if ( ! $product_id ) wp_die('Invalid product'); $product = wc_get_product($product_id); if ( ! $product ) wp_die('Product not found'); $type = isset($_POST['fpe_type']) ? sanitize_text_field($_POST['fpe_type']) : $product->get_type(); // SIMPLE if ( $type === 'simple' && $product->is_type('simple') ) { $new_raw = isset($_POST['fpe_simple_regular']) ? fpe_digits_only(wp_unslash($_POST['fpe_simple_regular'])) : ''; $old_raw = fpe_digits_only($product->get_regular_price()); if ($new_raw !== $old_raw) { $product->set_regular_price( $new_raw === '' ? '' : $new_raw ); $product->save(); wc_delete_product_transients($product_id); } wp_safe_redirect( get_permalink($product_id) . '?fpe_saved=1' ); exit; } // VARIABLE (✅ فقط variation های تغییر کرده save می‌شوند) if ( $type === 'variable' && $product->is_type('variable') ) { $regulars = (isset($_POST['fpe_regular']) && is_array($_POST['fpe_regular'])) ? $_POST['fpe_regular'] : []; $changed_any = false; foreach ( $product->get_children() as $variation_id ) { if ( ! array_key_exists($variation_id, $regulars) ) continue; $v = wc_get_product($variation_id); if (!$v) continue; $new_raw = fpe_digits_only( wp_unslash($regulars[$variation_id]) ); $old_raw = fpe_digits_only( $v->get_regular_price() ); // ✅ اگر تغییری نکرده، ذخیره نکن if ($new_raw === $old_raw) continue; $v->set_regular_price( $new_raw === '' ? '' : $new_raw ); $v->save(); $changed_any = true; } if ($changed_any) { wc_delete_product_transients($product_id); // عمداً $product->save(); نداریم تا تاریخ پدر بی‌دلیل آپدیت نشه } wp_safe_redirect( get_permalink($product_id) . '?fpe_saved=1' ); exit; } wp_die('Unsupported product type'); }); /** Toast */ add_action('wp_footer', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( isset($_GET['fpe_saved']) ) { echo '<div id="fpe-toast" style="position:fixed;bottom:18px;left:18px;z-index:999999;background:#111;color:#fff;padding:10px 12px;border-radius:14px;font-size:13px;">به‌روزرسانی انجام شد ✅</div>'; echo '<script>setTimeout(function(){var t=document.getElementById("fpe-toast"); if(t) t.remove();}, 3200);</script>'; } });
/**
 * Front-end Price Editor (Simple + Variable) - Code Snippets
 * ✅ فقط قیمت عادی (Regular) — بدون فروش ویژه
 * ✅ نمایش باکس جمع/بازشو + ریسپانسیو موبایل + رنگ‌بندی اکسل
 * ✅ فرمت سه‌تایی حین تایپ (23,000,000) ولی ذخیره امن (فقط ارقام)
 * ✅ فقط همان تنوعی که واقعاً تغییر کرده save می‌شود (تاریخ بقیه تنوع‌ها آپدیت نمی‌شود)
 * ✅ دکمه مثل وردپرس: «به‌روزرسانی»
 *
 * نصب: Code Snippets → Add New → PHP → Run everywhere → فعال
 */

if ( ! defined('ABSPATH') ) exit;

/** Optional: remove "choose an option" placeholder in variation dropdowns */
add_filter('woocommerce_dropdown_variation_attribute_options_args', function($args){
    $args['show_option_none'] = false;
    return $args;
});

/** Convert Persian/Arabic digits to English + keep only digits */
function fpe_digits_only($val){
    $val = (string) $val;
    $map = [
        '۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9',
        '٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9',
    ];
    $val = strtr($val, $map);
    return preg_replace('/\D+/', '', $val);
}

/** Variation label helpers */
function fpe_attribute_label($attr_key, $parent_product){
    $key = preg_replace('/^attribute_/', '', (string)$attr_key);

    if (strpos($key, 'pa_') === 0 && taxonomy_exists($key)) {
        $tax = get_taxonomy($key);
        if ($tax && ! empty($tax->labels->singular_name)) return $tax->labels->singular_name;
        return wc_attribute_label($key, $parent_product);
    }

    $label = wc_attribute_label($key, $parent_product);
    if ($label && $label !== $key) return $label;

    return str_replace(['pa_', '-', '_'], ['', ' ', ' '], $key);
}

function fpe_attribute_value_readable($taxonomy_or_name, $raw_val){
    $raw_val = (string)$raw_val;
    $decoded = rawurldecode($raw_val);
    $tax = preg_replace('/^attribute_/', '', (string)$taxonomy_or_name);

    if (strpos($tax, 'pa_') === 0 && taxonomy_exists($tax)) {
        $term = get_term_by('slug', $raw_val, $tax);
        if ( ! $term || is_wp_error($term) ) $term = get_term_by('slug', $decoded, $tax);
        if ( ! $term || is_wp_error($term) ) $term = get_term_by('name', $decoded, $tax);
        if ( $term && ! is_wp_error($term) ) return $term->name;
        return $decoded;
    }

    return $decoded;
}

function fpe_get_variation_label($variation, $parent_product){
    $out = [];
    foreach ((array)$variation->get_attributes() as $k => $v) {
        if ($v === '' || $v === null) continue;
        $out[] = fpe_attribute_label($k, $parent_product) . ': ' . fpe_attribute_value_readable($k, $v);
    }
    return $out ? implode(' | ', $out) : ('تنوع #' . $variation->get_id());
}

/** UI */
add_action('woocommerce_after_add_to_cart_form', function () {

    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( ! is_user_logged_in() ) return;
    if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) return;

    global $product;
    if ( ! $product ) return;

    $type = $product->get_type();
    if ( $type !== 'simple' && $type !== 'variable' ) return;

    $product_id = $product->get_id();
    $nonce = wp_create_nonce('fpe_save_prices');
    $title = ($type === 'simple') ? 'ویرایش قیمت محصول (ساده)' : 'ویرایش قیمت تنوع‌ها (متغیر)';

    echo '<style>
    .fpe-wrap{margin:16px 0;}
    .fpe-details{border:1px solid #e5e7eb;border-radius:14px;background:#fafafa;overflow:hidden;}
    .fpe-details>summary{list-style:none;cursor:pointer;padding:12px;display:flex;align-items:center;gap:10px;user-select:none;}
    .fpe-details>summary::-webkit-details-marker{display:none;}
    .fpe-badge{font-size:12px;padding:4px 10px;border-radius:999px;background:#111;color:#fff;white-space:nowrap;}
    .fpe-title{font-size:14px;font-weight:900;line-height:1.4;margin:0;flex:1;}
    .fpe-hint{font-size:12px;opacity:.7;margin:0;}
    .fpe-body{padding:12px;}

    .fpe-grid-head{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:10px;border-bottom:1px solid #e5e7eb;font-size:13px;font-weight:900;background:#f3f4f6;border-radius:12px;margin-bottom:10px;}
    .fpe-row{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:12px 10px;border:1px solid #e5e7eb;border-radius:14px;align-items:center;margin-bottom:10px;background:#fff;}
    .fpe-rows .fpe-row:nth-child(even){ background:#eef6ff; }

    .fpe-attr{font-size:13px;line-height:1.6;word-break:break-word;font-weight:800;}

    .fpe-input{
      width:100%;padding:10px;border:1px solid #c3c4c7;border-radius:10px;font-size:16px;outline:none;background:#fff;
      direction:ltr;text-align:center;
    }
    .fpe-input:focus{border-color:#2271b1; box-shadow:0 0 0 1px #2271b1;}

    .fpe-actions{margin-top:12px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;}
    .fpe-note{font-size:12px;opacity:.75;margin:0;}

    .fpe-btn{width:100%;padding:12px 18px;border:1px solid #2271b1;border-radius:6px;cursor:pointer;font-size:14px;font-weight:700;background:#2271b1;color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.08);}
    .fpe-btn:hover{background:#135e96;border-color:#135e96;}
    .fpe-btn:active{background:#0a4b78;border-color:#0a4b78;transform:translateY(1px);}

    @media (max-width:680px){
      .fpe-grid-head{display:none;}
      .fpe-row{grid-template-columns:1fr;gap:10px;padding:12px;}
      .fpe-field{display:flex;flex-direction:column;gap:6px;}
      .fpe-label{font-size:12px;opacity:.75;}
      .fpe-attr{font-size:14px;}
    }
    @media (min-width:681px){
      .fpe-btn{width:auto;min-width:220px;}
      .fpe-label{display:none;}
      .fpe-field{display:block;}
    }
    </style>';

    echo '<div class="fpe-wrap">';
    echo '<details class="fpe-details" '.(isset($_GET["fpe_saved"]) ? "open" : "").'>';
    echo '<summary><span class="fpe-badge">مدیر</span>
            <div style="min-width:0;">
              <p class="fpe-title">'.esc_html($title).'</p>
              <p class="fpe-hint">قیمت‌ها حین تایپ سه‌تایی جدا می‌شوند ✅</p>
            </div>
            <span style="opacity:.65;font-size:18px;">⌄</span>
          </summary>';

    echo '<div class="fpe-body"><form method="post" id="fpe-form">';
    echo '<input type="hidden" name="fpe_product_id" value="'.esc_attr($product_id).'">';
    echo '<input type="hidden" name="fpe_nonce" value="'.esc_attr($nonce).'">';
    echo '<input type="hidden" name="fpe_type" value="'.esc_attr($type).'">';

    echo '<div class="fpe-grid-head"><div>'.($type==='simple'?'محصول':'تنوع').'</div><div>قیمت</div></div>';
    echo '<div class="fpe-rows">';

    if ($type === 'simple') {
        $raw = fpe_digits_only($product->get_regular_price());
        echo '<div class="fpe-row">
                <div class="fpe-attr">این محصول</div>
                <div class="fpe-field">
                  <div class="fpe-label">قیمت</div>
                  <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_simple_regular" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000">
                </div>
              </div>';
    } else {
        foreach ($product->get_children() as $variation_id) {
            $v = wc_get_product($variation_id);
            if (!$v) continue;

            $label = fpe_get_variation_label($v, $product);
            $raw = fpe_digits_only($v->get_regular_price());

            echo '<div class="fpe-row">
                    <div class="fpe-attr">'.esc_html($label).'</div>
                    <div class="fpe-field">
                      <div class="fpe-label">قیمت</div>
                      <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_regular['.esc_attr($variation_id).']" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000">
                    </div>
                  </div>';
        }
    }

    echo '</div>';

    echo '<div class="fpe-actions">
            <button class="fpe-btn" type="submit" name="fpe_save" value="1">به‌روزرسانی</button>
            <p class="fpe-note">بعد از به‌روزرسانی، صفحه رفرش می‌شود.</p>
          </div>';

    echo '</form></div></details></div>';

    // JS: format while typing + keep caret position + submit digits-only
    echo '<script>
    (function(){
      function toEnDigits(s){
        if(!s) return "";
        var map = {"۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
        return String(s).replace(/[۰-۹٠-٩]/g, function(ch){ return map[ch] || ch; });
      }
      function digitsOnly(s){ return toEnDigits(s).replace(/\\D+/g,""); }
      function format3(s){
        s = digitsOnly(s);
        if(!s) return "";
        return s.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ",");
      }
      function caretDigitsIndex(value, caretPos){
        var left = value.slice(0, caretPos);
        return digitsOnly(left).length;
      }
      function caretFromDigitsIndex(formatted, digitIndex){
        var count = 0;
        for (var i=0; i<formatted.length; i++){
          if (/\\d/.test(formatted[i])) count++;
          if (count >= digitIndex) return i+1;
        }
        return formatted.length;
      }

      var inputs = document.querySelectorAll(".fpe-wrap .fpe-price");
      inputs.forEach(function(inp){
        inp.value = format3(inp.value);

        inp.addEventListener("input", function(){
          var oldVal = inp.value;
          var caret = inp.selectionStart || 0;

          var dIndex = caretDigitsIndex(oldVal, caret);
          var newVal = format3(oldVal);

          inp.value = newVal;

          var newCaret = caretFromDigitsIndex(newVal, dIndex);
          try { inp.setSelectionRange(newCaret, newCaret); } catch(err){}
        });

        inp.addEventListener("paste", function(){
          setTimeout(function(){ inp.value = format3(inp.value); }, 0);
        });
      });

      var form = document.getElementById("fpe-form");
      if(form){
        form.addEventListener("submit", function(){
          inputs.forEach(function(inp){ inp.value = digitsOnly(inp.value); });
        });
      }
    })();
    </script>';

}, 50);

/** Save handler (ONLY regular price) */
add_action('template_redirect', function () {

    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( empty($_POST['fpe_save']) ) return;

    if ( ! is_user_logged_in() ) wp_die('Access denied');
    if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) wp_die('Access denied');

    $nonce = isset($_POST['fpe_nonce']) ? sanitize_text_field($_POST['fpe_nonce']) : '';
    if ( ! wp_verify_nonce($nonce, 'fpe_save_prices') ) wp_die('Security check failed');

    $product_id = isset($_POST['fpe_product_id']) ? absint($_POST['fpe_product_id']) : 0;
    if ( ! $product_id ) wp_die('Invalid product');

    $product = wc_get_product($product_id);
    if ( ! $product ) wp_die('Product not found');

    $type = isset($_POST['fpe_type']) ? sanitize_text_field($_POST['fpe_type']) : $product->get_type();

    // SIMPLE
    if ( $type === 'simple' && $product->is_type('simple') ) {
        $new_raw = isset($_POST['fpe_simple_regular']) ? fpe_digits_only(wp_unslash($_POST['fpe_simple_regular'])) : '';
        $old_raw = fpe_digits_only($product->get_regular_price());

        if ($new_raw !== $old_raw) {
            $product->set_regular_price( $new_raw === '' ? '' : $new_raw );
            $product->save();
            wc_delete_product_transients($product_id);
        }

        wp_safe_redirect( get_permalink($product_id) . '?fpe_saved=1' );
        exit;
    }

    // VARIABLE (✅ فقط variation های تغییر کرده save می‌شوند)
    if ( $type === 'variable' && $product->is_type('variable') ) {
        $regulars = (isset($_POST['fpe_regular']) && is_array($_POST['fpe_regular'])) ? $_POST['fpe_regular'] : [];
        $changed_any = false;

        foreach ( $product->get_children() as $variation_id ) {
            if ( ! array_key_exists($variation_id, $regulars) ) continue;

            $v = wc_get_product($variation_id);
            if (!$v) continue;

            $new_raw = fpe_digits_only( wp_unslash($regulars[$variation_id]) );
            $old_raw = fpe_digits_only( $v->get_regular_price() );

            // ✅ اگر تغییری نکرده، ذخیره نکن
            if ($new_raw === $old_raw) continue;

            $v->set_regular_price( $new_raw === '' ? '' : $new_raw );
            $v->save();
            $changed_any = true;
        }

        if ($changed_any) {
            wc_delete_product_transients($product_id);
            // عمداً $product->save(); نداریم تا تاریخ پدر بی‌دلیل آپدیت نشه
        }

        wp_safe_redirect( get_permalink($product_id) . '?fpe_saved=1' );
        exit;
    }

    wp_die('Unsupported product type');
});

/** Toast */
add_action('wp_footer', function () {
    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( isset($_GET['fpe_saved']) ) {
        echo '<div id="fpe-toast" style="position:fixed;bottom:18px;left:18px;z-index:999999;background:#111;color:#fff;padding:10px 12px;border-radius:14px;font-size:13px;">به‌روزرسانی انجام شد ✅</div>';
        echo '<script>setTimeout(function(){var t=document.getElementById("fpe-toast"); if(t) t.remove();}, 3200);</script>';
    }
});
کد اصلاح شده تغییر قیمت
TEXT - 2026-05-18 10:05:33
/** * Front-end Price Editor (Simple + Variable) - Code Snippets * ✅ فقط قیمت عادی Regular * ✅ مناسب LiteSpeed Cache: بعد از تغییر قیمت، کش همان محصول پاک می‌شود */ if ( ! defined('ABSPATH') ) exit; /** Optional: remove "choose an option" placeholder in variation dropdowns */ add_filter('woocommerce_dropdown_variation_attribute_options_args', function($args){ $args['show_option_none'] = false; return $args; }); /** Convert Persian/Arabic digits to English + keep only digits */ function fpe_digits_only($val){ $val = (string) $val; $map = [ '۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9', '٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9', ]; $val = strtr($val, $map); return preg_replace('/\D+/', '', $val); } /** Purge product cache after price update */ function fpe_purge_product_cache($product_id){ $product_id = absint($product_id); if ( ! $product_id ) return; if ( function_exists('wc_delete_product_transients') ) { wc_delete_product_transients($product_id); } clean_post_cache($product_id); if ( function_exists('wc_get_product') ) { $product = wc_get_product($product_id); if ( $product && $product->is_type('variation') ) { $parent_id = $product->get_parent_id(); if ( $parent_id ) { wc_delete_product_transients($parent_id); clean_post_cache($parent_id); do_action('litespeed_purge_post', $parent_id); do_action('litespeed_purge_url', get_permalink($parent_id)); } } } do_action('litespeed_purge_post', $product_id); do_action('litespeed_purge_url', get_permalink($product_id)); } /** Variation label helpers */ function fpe_attribute_label($attr_key, $parent_product){ $key = preg_replace('/^attribute_/', '', (string)$attr_key); if (strpos($key, 'pa_') === 0 && taxonomy_exists($key)) { $tax = get_taxonomy($key); if ($tax && ! empty($tax->labels->singular_name)) return $tax->labels->singular_name; return wc_attribute_label($key, $parent_product); } $label = wc_attribute_label($key, $parent_product); if ($label && $label !== $key) return $label; return str_replace(['pa_', '-', '_'], ['', ' ', ' '], $key); } function fpe_attribute_value_readable($taxonomy_or_name, $raw_val){ $raw_val = (string)$raw_val; $decoded = rawurldecode($raw_val); $tax = preg_replace('/^attribute_/', '', (string)$taxonomy_or_name); if (strpos($tax, 'pa_') === 0 && taxonomy_exists($tax)) { $term = get_term_by('slug', $raw_val, $tax); if ( ! $term || is_wp_error($term) ) $term = get_term_by('slug', $decoded, $tax); if ( ! $term || is_wp_error($term) ) $term = get_term_by('name', $decoded, $tax); if ( $term && ! is_wp_error($term) ) return $term->name; return $decoded; } return $decoded; } function fpe_get_variation_label($variation, $parent_product){ $out = []; foreach ((array)$variation->get_attributes() as $k => $v) { if ($v === '' || $v === null) continue; $out[] = fpe_attribute_label($k, $parent_product) . ': ' . fpe_attribute_value_readable($k, $v); } return $out ? implode(' | ', $out) : ('تنوع #' . $variation->get_id()); } /** UI */ add_action('woocommerce_after_add_to_cart_form', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( ! is_user_logged_in() ) return; if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) return; global $product; if ( ! $product ) return; $type = $product->get_type(); if ( $type !== 'simple' && $type !== 'variable' ) return; $product_id = $product->get_id(); $nonce = wp_create_nonce('fpe_save_prices'); $title = ($type === 'simple') ? 'ویرایش قیمت محصول ساده' : 'ویرایش قیمت تنوع‌ها'; echo '<style> .fpe-wrap{margin:16px 0;} .fpe-details{border:1px solid #e5e7eb;border-radius:14px;background:#fafafa;overflow:hidden;} .fpe-details>summary{list-style:none;cursor:pointer;padding:12px;display:flex;align-items:center;gap:10px;user-select:none;} .fpe-details>summary::-webkit-details-marker{display:none;} .fpe-badge{font-size:12px;padding:4px 10px;border-radius:999px;background:#111;color:#fff;white-space:nowrap;} .fpe-title{font-size:14px;font-weight:900;line-height:1.4;margin:0;flex:1;} .fpe-hint{font-size:12px;opacity:.7;margin:0;} .fpe-body{padding:12px;} .fpe-grid-head{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:10px;border-bottom:1px solid #e5e7eb;font-size:13px;font-weight:900;background:#f3f4f6;border-radius:12px;margin-bottom:10px;} .fpe-row{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:12px 10px;border:1px solid #e5e7eb;border-radius:14px;align-items:center;margin-bottom:10px;background:#fff;} .fpe-rows .fpe-row:nth-child(even){ background:#eef6ff; } .fpe-attr{font-size:13px;line-height:1.6;word-break:break-word;font-weight:800;} .fpe-input{ width:100%;padding:10px;border:1px solid #c3c4c7;border-radius:10px;font-size:16px;outline:none;background:#fff; direction:ltr;text-align:center; } .fpe-input:focus{border-color:#2271b1; box-shadow:0 0 0 1px #2271b1;} .fpe-actions{margin-top:12px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;} .fpe-note{font-size:12px;opacity:.75;margin:0;} .fpe-btn{width:100%;padding:12px 18px;border:1px solid #2271b1;border-radius:6px;cursor:pointer;font-size:14px;font-weight:700;background:#2271b1;color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.08);} .fpe-btn:hover{background:#135e96;border-color:#135e96;} .fpe-btn:active{background:#0a4b78;border-color:#0a4b78;transform:translateY(1px);} @media (max-width:680px){ .fpe-grid-head{display:none;} .fpe-row{grid-template-columns:1fr;gap:10px;padding:12px;} .fpe-field{display:flex;flex-direction:column;gap:6px;} .fpe-label{font-size:12px;opacity:.75;} .fpe-attr{font-size:14px;} } @media (min-width:681px){ .fpe-btn{width:auto;min-width:220px;} .fpe-label{display:none;} .fpe-field{display:block;} } </style>'; echo '<div class="fpe-wrap">'; echo '<details class="fpe-details" '.(isset($_GET["fpe_saved"]) ? "open" : "").'>'; echo '<summary><span class="fpe-badge">مدیر</span> <div style="min-width:0;"> <p class="fpe-title">'.esc_html($title).'</p> <p class="fpe-hint">قیمت‌ها حین تایپ سه‌تایی جدا می‌شوند</p> </div> <span style="opacity:.65;font-size:18px;">⌄</span> </summary>'; echo '<div class="fpe-body"><form method="post" id="fpe-form">'; echo '<input type="hidden" name="fpe_product_id" value="'.esc_attr($product_id).'">'; echo '<input type="hidden" name="fpe_nonce" value="'.esc_attr($nonce).'">'; echo '<input type="hidden" name="fpe_type" value="'.esc_attr($type).'">'; echo '<div class="fpe-grid-head"><div>'.($type==='simple'?'محصول':'تنوع').'</div><div>قیمت</div></div>'; echo '<div class="fpe-rows">'; if ($type === 'simple') { $raw = fpe_digits_only($product->get_regular_price()); echo '<div class="fpe-row"> <div class="fpe-attr">این محصول</div> <div class="fpe-field"> <div class="fpe-label">قیمت</div> <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_simple_regular" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000"> </div> </div>'; } else { foreach ($product->get_children() as $variation_id) { $v = wc_get_product($variation_id); if (!$v) continue; $label = fpe_get_variation_label($v, $product); $raw = fpe_digits_only($v->get_regular_price()); echo '<div class="fpe-row"> <div class="fpe-attr">'.esc_html($label).'</div> <div class="fpe-field"> <div class="fpe-label">قیمت</div> <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_regular['.esc_attr($variation_id).']" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000"> </div> </div>'; } } echo '</div>'; echo '<div class="fpe-actions"> <button class="fpe-btn" type="submit" name="fpe_save" value="1">به‌روزرسانی</button> <p class="fpe-note">بعد از به‌روزرسانی، کش همان محصول پاک می‌شود.</p> </div>'; echo '</form></div></details></div>'; echo '<script> (function(){ function toEnDigits(s){ if(!s) return ""; var map = {"۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"}; return String(s).replace(/[۰-۹٠-٩]/g, function(ch){ return map[ch] || ch; }); } function digitsOnly(s){ return toEnDigits(s).replace(/\\D+/g,""); } function format3(s){ s = digitsOnly(s); if(!s) return ""; return s.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ","); } function caretDigitsIndex(value, caretPos){ var left = value.slice(0, caretPos); return digitsOnly(left).length; } function caretFromDigitsIndex(formatted, digitIndex){ var count = 0; for (var i=0; i<formatted.length; i++){ if (/\\d/.test(formatted[i])) count++; if (count >= digitIndex) return i+1; } return formatted.length; } var inputs = document.querySelectorAll(".fpe-wrap .fpe-price"); inputs.forEach(function(inp){ inp.value = format3(inp.value); inp.addEventListener("input", function(){ var oldVal = inp.value; var caret = inp.selectionStart || 0; var dIndex = caretDigitsIndex(oldVal, caret); var newVal = format3(oldVal); inp.value = newVal; var newCaret = caretFromDigitsIndex(newVal, dIndex); try { inp.setSelectionRange(newCaret, newCaret); } catch(err){} }); inp.addEventListener("paste", function(){ setTimeout(function(){ inp.value = format3(inp.value); }, 0); }); }); var form = document.getElementById("fpe-form"); if(form){ form.addEventListener("submit", function(){ inputs.forEach(function(inp){ inp.value = digitsOnly(inp.value); }); }); } })(); </script>'; }, 50); /** Save handler */ add_action('template_redirect', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( empty($_POST['fpe_save']) ) return; if ( ! is_user_logged_in() ) wp_die('Access denied'); if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) wp_die('Access denied'); $nonce = isset($_POST['fpe_nonce']) ? sanitize_text_field($_POST['fpe_nonce']) : ''; if ( ! wp_verify_nonce($nonce, 'fpe_save_prices') ) wp_die('Security check failed'); $product_id = isset($_POST['fpe_product_id']) ? absint($_POST['fpe_product_id']) : 0; if ( ! $product_id ) wp_die('Invalid product'); $product = wc_get_product($product_id); if ( ! $product ) wp_die('Product not found'); $type = isset($_POST['fpe_type']) ? sanitize_text_field($_POST['fpe_type']) : $product->get_type(); // SIMPLE if ( $type === 'simple' && $product->is_type('simple') ) { $new_raw = isset($_POST['fpe_simple_regular']) ? fpe_digits_only(wp_unslash($_POST['fpe_simple_regular'])) : ''; $old_raw = fpe_digits_only($product->get_regular_price()); if ($new_raw !== $old_raw) { $product->set_regular_price( $new_raw === '' ? '' : $new_raw ); $product->save(); fpe_purge_product_cache($product_id); } wp_safe_redirect( add_query_arg('fpe_saved', '1', get_permalink($product_id)) ); exit; } // VARIABLE if ( $type === 'variable' && $product->is_type('variable') ) { $regulars = (isset($_POST['fpe_regular']) && is_array($_POST['fpe_regular'])) ? $_POST['fpe_regular'] : []; $changed_any = false; foreach ( $product->get_children() as $variation_id ) { if ( ! array_key_exists($variation_id, $regulars) ) continue; $v = wc_get_product($variation_id); if (!$v) continue; $new_raw = fpe_digits_only( wp_unslash($regulars[$variation_id]) ); $old_raw = fpe_digits_only( $v->get_regular_price() ); if ($new_raw === $old_raw) continue; $v->set_regular_price( $new_raw === '' ? '' : $new_raw ); $v->save(); fpe_purge_product_cache($variation_id); $changed_any = true; } if ($changed_any) { fpe_purge_product_cache($product_id); } wp_safe_redirect( add_query_arg('fpe_saved', '1', get_permalink($product_id)) ); exit; } wp_die('Unsupported product type'); }); /** Toast */ add_action('wp_footer', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( isset($_GET['fpe_saved']) ) { echo '<div id="fpe-toast" style="position:fixed;bottom:18px;left:18px;z-index:999999;background:#111;color:#fff;padding:10px 12px;border-radius:14px;font-size:13px;">به‌روزرسانی انجام شد ✅</div>'; echo '<script>setTimeout(function(){var t=document.getElementById("fpe-toast"); if(t) t.remove();}, 3200);</script>'; } });
/**
 * Front-end Price Editor (Simple + Variable) - Code Snippets
 * ✅ فقط قیمت عادی Regular
 * ✅ مناسب LiteSpeed Cache: بعد از تغییر قیمت، کش همان محصول پاک می‌شود
 */

if ( ! defined('ABSPATH') ) exit;

/** Optional: remove "choose an option" placeholder in variation dropdowns */
add_filter('woocommerce_dropdown_variation_attribute_options_args', function($args){
    $args['show_option_none'] = false;
    return $args;
});

/** Convert Persian/Arabic digits to English + keep only digits */
function fpe_digits_only($val){
    $val = (string) $val;
    $map = [
        '۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9',
        '٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9',
    ];
    $val = strtr($val, $map);
    return preg_replace('/\D+/', '', $val);
}

/** Purge product cache after price update */
function fpe_purge_product_cache($product_id){
    $product_id = absint($product_id);
    if ( ! $product_id ) return;

    if ( function_exists('wc_delete_product_transients') ) {
        wc_delete_product_transients($product_id);
    }

    clean_post_cache($product_id);

    if ( function_exists('wc_get_product') ) {
        $product = wc_get_product($product_id);

        if ( $product && $product->is_type('variation') ) {
            $parent_id = $product->get_parent_id();
            if ( $parent_id ) {
                wc_delete_product_transients($parent_id);
                clean_post_cache($parent_id);

                do_action('litespeed_purge_post', $parent_id);
                do_action('litespeed_purge_url', get_permalink($parent_id));
            }
        }
    }

    do_action('litespeed_purge_post', $product_id);
    do_action('litespeed_purge_url', get_permalink($product_id));
}

/** Variation label helpers */
function fpe_attribute_label($attr_key, $parent_product){
    $key = preg_replace('/^attribute_/', '', (string)$attr_key);

    if (strpos($key, 'pa_') === 0 && taxonomy_exists($key)) {
        $tax = get_taxonomy($key);
        if ($tax && ! empty($tax->labels->singular_name)) return $tax->labels->singular_name;
        return wc_attribute_label($key, $parent_product);
    }

    $label = wc_attribute_label($key, $parent_product);
    if ($label && $label !== $key) return $label;

    return str_replace(['pa_', '-', '_'], ['', ' ', ' '], $key);
}

function fpe_attribute_value_readable($taxonomy_or_name, $raw_val){
    $raw_val = (string)$raw_val;
    $decoded = rawurldecode($raw_val);
    $tax = preg_replace('/^attribute_/', '', (string)$taxonomy_or_name);

    if (strpos($tax, 'pa_') === 0 && taxonomy_exists($tax)) {
        $term = get_term_by('slug', $raw_val, $tax);
        if ( ! $term || is_wp_error($term) ) $term = get_term_by('slug', $decoded, $tax);
        if ( ! $term || is_wp_error($term) ) $term = get_term_by('name', $decoded, $tax);
        if ( $term && ! is_wp_error($term) ) return $term->name;
        return $decoded;
    }

    return $decoded;
}

function fpe_get_variation_label($variation, $parent_product){
    $out = [];
    foreach ((array)$variation->get_attributes() as $k => $v) {
        if ($v === '' || $v === null) continue;
        $out[] = fpe_attribute_label($k, $parent_product) . ': ' . fpe_attribute_value_readable($k, $v);
    }
    return $out ? implode(' | ', $out) : ('تنوع #' . $variation->get_id());
}

/** UI */
add_action('woocommerce_after_add_to_cart_form', function () {

    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( ! is_user_logged_in() ) return;
    if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) return;

    global $product;
    if ( ! $product ) return;

    $type = $product->get_type();
    if ( $type !== 'simple' && $type !== 'variable' ) return;

    $product_id = $product->get_id();
    $nonce = wp_create_nonce('fpe_save_prices');
    $title = ($type === 'simple') ? 'ویرایش قیمت محصول ساده' : 'ویرایش قیمت تنوع‌ها';

    echo '<style>
    .fpe-wrap{margin:16px 0;}
    .fpe-details{border:1px solid #e5e7eb;border-radius:14px;background:#fafafa;overflow:hidden;}
    .fpe-details>summary{list-style:none;cursor:pointer;padding:12px;display:flex;align-items:center;gap:10px;user-select:none;}
    .fpe-details>summary::-webkit-details-marker{display:none;}
    .fpe-badge{font-size:12px;padding:4px 10px;border-radius:999px;background:#111;color:#fff;white-space:nowrap;}
    .fpe-title{font-size:14px;font-weight:900;line-height:1.4;margin:0;flex:1;}
    .fpe-hint{font-size:12px;opacity:.7;margin:0;}
    .fpe-body{padding:12px;}

    .fpe-grid-head{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:10px;border-bottom:1px solid #e5e7eb;font-size:13px;font-weight:900;background:#f3f4f6;border-radius:12px;margin-bottom:10px;}
    .fpe-row{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:12px 10px;border:1px solid #e5e7eb;border-radius:14px;align-items:center;margin-bottom:10px;background:#fff;}
    .fpe-rows .fpe-row:nth-child(even){ background:#eef6ff; }

    .fpe-attr{font-size:13px;line-height:1.6;word-break:break-word;font-weight:800;}

    .fpe-input{
      width:100%;padding:10px;border:1px solid #c3c4c7;border-radius:10px;font-size:16px;outline:none;background:#fff;
      direction:ltr;text-align:center;
    }
    .fpe-input:focus{border-color:#2271b1; box-shadow:0 0 0 1px #2271b1;}

    .fpe-actions{margin-top:12px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;}
    .fpe-note{font-size:12px;opacity:.75;margin:0;}

    .fpe-btn{width:100%;padding:12px 18px;border:1px solid #2271b1;border-radius:6px;cursor:pointer;font-size:14px;font-weight:700;background:#2271b1;color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.08);}
    .fpe-btn:hover{background:#135e96;border-color:#135e96;}
    .fpe-btn:active{background:#0a4b78;border-color:#0a4b78;transform:translateY(1px);}

    @media (max-width:680px){
      .fpe-grid-head{display:none;}
      .fpe-row{grid-template-columns:1fr;gap:10px;padding:12px;}
      .fpe-field{display:flex;flex-direction:column;gap:6px;}
      .fpe-label{font-size:12px;opacity:.75;}
      .fpe-attr{font-size:14px;}
    }
    @media (min-width:681px){
      .fpe-btn{width:auto;min-width:220px;}
      .fpe-label{display:none;}
      .fpe-field{display:block;}
    }
    </style>';

    echo '<div class="fpe-wrap">';
    echo '<details class="fpe-details" '.(isset($_GET["fpe_saved"]) ? "open" : "").'>';
    echo '<summary><span class="fpe-badge">مدیر</span>
            <div style="min-width:0;">
              <p class="fpe-title">'.esc_html($title).'</p>
              <p class="fpe-hint">قیمت‌ها حین تایپ سه‌تایی جدا می‌شوند</p>
            </div>
            <span style="opacity:.65;font-size:18px;">⌄</span>
          </summary>';

    echo '<div class="fpe-body"><form method="post" id="fpe-form">';
    echo '<input type="hidden" name="fpe_product_id" value="'.esc_attr($product_id).'">';
    echo '<input type="hidden" name="fpe_nonce" value="'.esc_attr($nonce).'">';
    echo '<input type="hidden" name="fpe_type" value="'.esc_attr($type).'">';

    echo '<div class="fpe-grid-head"><div>'.($type==='simple'?'محصول':'تنوع').'</div><div>قیمت</div></div>';
    echo '<div class="fpe-rows">';

    if ($type === 'simple') {
        $raw = fpe_digits_only($product->get_regular_price());

        echo '<div class="fpe-row">
                <div class="fpe-attr">این محصول</div>
                <div class="fpe-field">
                  <div class="fpe-label">قیمت</div>
                  <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_simple_regular" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000">
                </div>
              </div>';

    } else {
        foreach ($product->get_children() as $variation_id) {
            $v = wc_get_product($variation_id);
            if (!$v) continue;

            $label = fpe_get_variation_label($v, $product);
            $raw = fpe_digits_only($v->get_regular_price());

            echo '<div class="fpe-row">
                    <div class="fpe-attr">'.esc_html($label).'</div>
                    <div class="fpe-field">
                      <div class="fpe-label">قیمت</div>
                      <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_regular['.esc_attr($variation_id).']" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000">
                    </div>
                  </div>';
        }
    }

    echo '</div>';

    echo '<div class="fpe-actions">
            <button class="fpe-btn" type="submit" name="fpe_save" value="1">به‌روزرسانی</button>
            <p class="fpe-note">بعد از به‌روزرسانی، کش همان محصول پاک می‌شود.</p>
          </div>';

    echo '</form></div></details></div>';

    echo '<script>
    (function(){
      function toEnDigits(s){
        if(!s) return "";
        var map = {"۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
        return String(s).replace(/[۰-۹٠-٩]/g, function(ch){ return map[ch] || ch; });
      }
      function digitsOnly(s){ return toEnDigits(s).replace(/\\D+/g,""); }
      function format3(s){
        s = digitsOnly(s);
        if(!s) return "";
        return s.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ",");
      }
      function caretDigitsIndex(value, caretPos){
        var left = value.slice(0, caretPos);
        return digitsOnly(left).length;
      }
      function caretFromDigitsIndex(formatted, digitIndex){
        var count = 0;
        for (var i=0; i<formatted.length; i++){
          if (/\\d/.test(formatted[i])) count++;
          if (count >= digitIndex) return i+1;
        }
        return formatted.length;
      }

      var inputs = document.querySelectorAll(".fpe-wrap .fpe-price");
      inputs.forEach(function(inp){
        inp.value = format3(inp.value);

        inp.addEventListener("input", function(){
          var oldVal = inp.value;
          var caret = inp.selectionStart || 0;
          var dIndex = caretDigitsIndex(oldVal, caret);
          var newVal = format3(oldVal);
          inp.value = newVal;
          var newCaret = caretFromDigitsIndex(newVal, dIndex);
          try { inp.setSelectionRange(newCaret, newCaret); } catch(err){}
        });

        inp.addEventListener("paste", function(){
          setTimeout(function(){ inp.value = format3(inp.value); }, 0);
        });
      });

      var form = document.getElementById("fpe-form");
      if(form){
        form.addEventListener("submit", function(){
          inputs.forEach(function(inp){ inp.value = digitsOnly(inp.value); });
        });
      }
    })();
    </script>';

}, 50);

/** Save handler */
add_action('template_redirect', function () {

    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( empty($_POST['fpe_save']) ) return;

    if ( ! is_user_logged_in() ) wp_die('Access denied');
    if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) wp_die('Access denied');

    $nonce = isset($_POST['fpe_nonce']) ? sanitize_text_field($_POST['fpe_nonce']) : '';
    if ( ! wp_verify_nonce($nonce, 'fpe_save_prices') ) wp_die('Security check failed');

    $product_id = isset($_POST['fpe_product_id']) ? absint($_POST['fpe_product_id']) : 0;
    if ( ! $product_id ) wp_die('Invalid product');

    $product = wc_get_product($product_id);
    if ( ! $product ) wp_die('Product not found');

    $type = isset($_POST['fpe_type']) ? sanitize_text_field($_POST['fpe_type']) : $product->get_type();

    // SIMPLE
    if ( $type === 'simple' && $product->is_type('simple') ) {
        $new_raw = isset($_POST['fpe_simple_regular']) ? fpe_digits_only(wp_unslash($_POST['fpe_simple_regular'])) : '';
        $old_raw = fpe_digits_only($product->get_regular_price());

        if ($new_raw !== $old_raw) {
            $product->set_regular_price( $new_raw === '' ? '' : $new_raw );
            $product->save();

            fpe_purge_product_cache($product_id);
        }

        wp_safe_redirect( add_query_arg('fpe_saved', '1', get_permalink($product_id)) );
        exit;
    }

    // VARIABLE
    if ( $type === 'variable' && $product->is_type('variable') ) {
        $regulars = (isset($_POST['fpe_regular']) && is_array($_POST['fpe_regular'])) ? $_POST['fpe_regular'] : [];
        $changed_any = false;

        foreach ( $product->get_children() as $variation_id ) {
            if ( ! array_key_exists($variation_id, $regulars) ) continue;

            $v = wc_get_product($variation_id);
            if (!$v) continue;

            $new_raw = fpe_digits_only( wp_unslash($regulars[$variation_id]) );
            $old_raw = fpe_digits_only( $v->get_regular_price() );

            if ($new_raw === $old_raw) continue;

            $v->set_regular_price( $new_raw === '' ? '' : $new_raw );
            $v->save();

            fpe_purge_product_cache($variation_id);

            $changed_any = true;
        }

        if ($changed_any) {
            fpe_purge_product_cache($product_id);
        }

        wp_safe_redirect( add_query_arg('fpe_saved', '1', get_permalink($product_id)) );
        exit;
    }

    wp_die('Unsupported product type');
});

/** Toast */
add_action('wp_footer', function () {
    if ( ! function_exists('is_product') || ! is_product() ) return;

    if ( isset($_GET['fpe_saved']) ) {
        echo '<div id="fpe-toast" style="position:fixed;bottom:18px;left:18px;z-index:999999;background:#111;color:#fff;padding:10px 12px;border-radius:14px;font-size:13px;">به‌روزرسانی انجام شد ✅</div>';
        echo '<script>setTimeout(function(){var t=document.getElementById("fpe-toast"); if(t) t.remove();}, 3200);</script>';
    }
});
تغییر قیمت
TEXT - 2026-05-18 09:53:53
/** * Front-end Price Editor (Simple + Variable) - Code Snippets * ✅ فقط قیمت عادی (Regular) — بدون فروش ویژه * ✅ نمایش باکس جمع/بازشو + ریسپانسیو موبایل + رنگ‌بندی اکسل * ✅ فرمت سه‌تایی حین تایپ (23,000,000) ولی ذخیره امن (فقط ارقام) * ✅ فقط همان تنوعی که واقعاً تغییر کرده save می‌شود (تاریخ بقیه تنوع‌ها آپدیت نمی‌شود) * ✅ دکمه مثل وردپرس: «به‌روزرسانی» * * نصب: Code Snippets → Add New → PHP → Run everywhere → فعال */ if ( ! defined('ABSPATH') ) exit; /** Optional: remove "choose an option" placeholder in variation dropdowns */ add_filter('woocommerce_dropdown_variation_attribute_options_args', function($args){ $args['show_option_none'] = false; return $args; }); /** Convert Persian/Arabic digits to English + keep only digits */ function fpe_digits_only($val){ $val = (string) $val; $map = [ '۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9', '٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9', ]; $val = strtr($val, $map); return preg_replace('/\D+/', '', $val); } /** Variation label helpers */ function fpe_attribute_label($attr_key, $parent_product){ $key = preg_replace('/^attribute_/', '', (string)$attr_key); if (strpos($key, 'pa_') === 0 && taxonomy_exists($key)) { $tax = get_taxonomy($key); if ($tax && ! empty($tax->labels->singular_name)) return $tax->labels->singular_name; return wc_attribute_label($key, $parent_product); } $label = wc_attribute_label($key, $parent_product); if ($label && $label !== $key) return $label; return str_replace(['pa_', '-', '_'], ['', ' ', ' '], $key); } function fpe_attribute_value_readable($taxonomy_or_name, $raw_val){ $raw_val = (string)$raw_val; $decoded = rawurldecode($raw_val); $tax = preg_replace('/^attribute_/', '', (string)$taxonomy_or_name); if (strpos($tax, 'pa_') === 0 && taxonomy_exists($tax)) { $term = get_term_by('slug', $raw_val, $tax); if ( ! $term || is_wp_error($term) ) $term = get_term_by('slug', $decoded, $tax); if ( ! $term || is_wp_error($term) ) $term = get_term_by('name', $decoded, $tax); if ( $term && ! is_wp_error($term) ) return $term->name; return $decoded; } return $decoded; } function fpe_get_variation_label($variation, $parent_product){ $out = []; foreach ((array)$variation->get_attributes() as $k => $v) { if ($v === '' || $v === null) continue; $out[] = fpe_attribute_label($k, $parent_product) . ': ' . fpe_attribute_value_readable($k, $v); } return $out ? implode(' | ', $out) : ('تنوع #' . $variation->get_id()); } /** UI */ add_action('woocommerce_after_add_to_cart_form', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( ! is_user_logged_in() ) return; if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) return; global $product; if ( ! $product ) return; $type = $product->get_type(); if ( $type !== 'simple' && $type !== 'variable' ) return; $product_id = $product->get_id(); $nonce = wp_create_nonce('fpe_save_prices'); $title = ($type === 'simple') ? 'ویرایش قیمت محصول (ساده)' : 'ویرایش قیمت تنوع‌ها (متغیر)'; echo '<style> .fpe-wrap{margin:16px 0;} .fpe-details{border:1px solid #e5e7eb;border-radius:14px;background:#fafafa;overflow:hidden;} .fpe-details>summary{list-style:none;cursor:pointer;padding:12px;display:flex;align-items:center;gap:10px;user-select:none;} .fpe-details>summary::-webkit-details-marker{display:none;} .fpe-badge{font-size:12px;padding:4px 10px;border-radius:999px;background:#111;color:#fff;white-space:nowrap;} .fpe-title{font-size:14px;font-weight:900;line-height:1.4;margin:0;flex:1;} .fpe-hint{font-size:12px;opacity:.7;margin:0;} .fpe-body{padding:12px;} .fpe-grid-head{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:10px;border-bottom:1px solid #e5e7eb;font-size:13px;font-weight:900;background:#f3f4f6;border-radius:12px;margin-bottom:10px;} .fpe-row{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:12px 10px;border:1px solid #e5e7eb;border-radius:14px;align-items:center;margin-bottom:10px;background:#fff;} .fpe-rows .fpe-row:nth-child(even){ background:#eef6ff; } .fpe-attr{font-size:13px;line-height:1.6;word-break:break-word;font-weight:800;} .fpe-input{ width:100%;padding:10px;border:1px solid #c3c4c7;border-radius:10px;font-size:16px;outline:none;background:#fff; direction:ltr;text-align:center; } .fpe-input:focus{border-color:#2271b1; box-shadow:0 0 0 1px #2271b1;} .fpe-actions{margin-top:12px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;} .fpe-note{font-size:12px;opacity:.75;margin:0;} .fpe-btn{width:100%;padding:12px 18px;border:1px solid #2271b1;border-radius:6px;cursor:pointer;font-size:14px;font-weight:700;background:#2271b1;color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.08);} .fpe-btn:hover{background:#135e96;border-color:#135e96;} .fpe-btn:active{background:#0a4b78;border-color:#0a4b78;transform:translateY(1px);} @media (max-width:680px){ .fpe-grid-head{display:none;} .fpe-row{grid-template-columns:1fr;gap:10px;padding:12px;} .fpe-field{display:flex;flex-direction:column;gap:6px;} .fpe-label{font-size:12px;opacity:.75;} .fpe-attr{font-size:14px;} } @media (min-width:681px){ .fpe-btn{width:auto;min-width:220px;} .fpe-label{display:none;} .fpe-field{display:block;} } </style>'; echo '<div class="fpe-wrap">'; echo '<details class="fpe-details" '.(isset($_GET["fpe_saved"]) ? "open" : "").'>'; echo '<summary><span class="fpe-badge">مدیر</span> <div style="min-width:0;"> <p class="fpe-title">'.esc_html($title).'</p> <p class="fpe-hint">قیمت‌ها حین تایپ سه‌تایی جدا می‌شوند ✅</p> </div> <span style="opacity:.65;font-size:18px;">⌄</span> </summary>'; echo '<div class="fpe-body"><form method="post" id="fpe-form">'; echo '<input type="hidden" name="fpe_product_id" value="'.esc_attr($product_id).'">'; echo '<input type="hidden" name="fpe_nonce" value="'.esc_attr($nonce).'">'; echo '<input type="hidden" name="fpe_type" value="'.esc_attr($type).'">'; echo '<div class="fpe-grid-head"><div>'.($type==='simple'?'محصول':'تنوع').'</div><div>قیمت</div></div>'; echo '<div class="fpe-rows">'; if ($type === 'simple') { $raw = fpe_digits_only($product->get_regular_price()); echo '<div class="fpe-row"> <div class="fpe-attr">این محصول</div> <div class="fpe-field"> <div class="fpe-label">قیمت</div> <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_simple_regular" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000"> </div> </div>'; } else { foreach ($product->get_children() as $variation_id) { $v = wc_get_product($variation_id); if (!$v) continue; $label = fpe_get_variation_label($v, $product); $raw = fpe_digits_only($v->get_regular_price()); echo '<div class="fpe-row"> <div class="fpe-attr">'.esc_html($label).'</div> <div class="fpe-field"> <div class="fpe-label">قیمت</div> <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_regular['.esc_attr($variation_id).']" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000"> </div> </div>'; } } echo '</div>'; echo '<div class="fpe-actions"> <button class="fpe-btn" type="submit" name="fpe_save" value="1">به‌روزرسانی</button> <p class="fpe-note">بعد از به‌روزرسانی، صفحه رفرش می‌شود.</p> </div>'; echo '</form></div></details></div>'; // JS: format while typing + keep caret position + submit digits-only echo '<script> (function(){ function toEnDigits(s){ if(!s) return ""; var map = {"۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"}; return String(s).replace(/[۰-۹٠-٩]/g, function(ch){ return map[ch] || ch; }); } function digitsOnly(s){ return toEnDigits(s).replace(/\\D+/g,""); } function format3(s){ s = digitsOnly(s); if(!s) return ""; return s.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ","); } function caretDigitsIndex(value, caretPos){ var left = value.slice(0, caretPos); return digitsOnly(left).length; } function caretFromDigitsIndex(formatted, digitIndex){ var count = 0; for (var i=0; i<formatted.length; i++){ if (/\\d/.test(formatted[i])) count++; if (count >= digitIndex) return i+1; } return formatted.length; } var inputs = document.querySelectorAll(".fpe-wrap .fpe-price"); inputs.forEach(function(inp){ inp.value = format3(inp.value); inp.addEventListener("input", function(){ var oldVal = inp.value; var caret = inp.selectionStart || 0; var dIndex = caretDigitsIndex(oldVal, caret); var newVal = format3(oldVal); inp.value = newVal; var newCaret = caretFromDigitsIndex(newVal, dIndex); try { inp.setSelectionRange(newCaret, newCaret); } catch(err){} }); inp.addEventListener("paste", function(){ setTimeout(function(){ inp.value = format3(inp.value); }, 0); }); }); var form = document.getElementById("fpe-form"); if(form){ form.addEventListener("submit", function(){ inputs.forEach(function(inp){ inp.value = digitsOnly(inp.value); }); }); } })(); </script>'; }, 50); /** Save handler (ONLY regular price) */ add_action('template_redirect', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( empty($_POST['fpe_save']) ) return; if ( ! is_user_logged_in() ) wp_die('Access denied'); if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) wp_die('Access denied'); $nonce = isset($_POST['fpe_nonce']) ? sanitize_text_field($_POST['fpe_nonce']) : ''; if ( ! wp_verify_nonce($nonce, 'fpe_save_prices') ) wp_die('Security check failed'); $product_id = isset($_POST['fpe_product_id']) ? absint($_POST['fpe_product_id']) : 0; if ( ! $product_id ) wp_die('Invalid product'); $product = wc_get_product($product_id); if ( ! $product ) wp_die('Product not found'); $type = isset($_POST['fpe_type']) ? sanitize_text_field($_POST['fpe_type']) : $product->get_type(); // SIMPLE if ( $type === 'simple' && $product->is_type('simple') ) { $new_raw = isset($_POST['fpe_simple_regular']) ? fpe_digits_only(wp_unslash($_POST['fpe_simple_regular'])) : ''; $old_raw = fpe_digits_only($product->get_regular_price()); if ($new_raw !== $old_raw) { $product->set_regular_price( $new_raw === '' ? '' : $new_raw ); $product->save(); wc_delete_product_transients($product_id); } wp_safe_redirect( get_permalink($product_id) . '?fpe_saved=1' ); exit; } // VARIABLE (✅ فقط variation های تغییر کرده save می‌شوند) if ( $type === 'variable' && $product->is_type('variable') ) { $regulars = (isset($_POST['fpe_regular']) && is_array($_POST['fpe_regular'])) ? $_POST['fpe_regular'] : []; $changed_any = false; foreach ( $product->get_children() as $variation_id ) { if ( ! array_key_exists($variation_id, $regulars) ) continue; $v = wc_get_product($variation_id); if (!$v) continue; $new_raw = fpe_digits_only( wp_unslash($regulars[$variation_id]) ); $old_raw = fpe_digits_only( $v->get_regular_price() ); // ✅ اگر تغییری نکرده، ذخیره نکن if ($new_raw === $old_raw) continue; $v->set_regular_price( $new_raw === '' ? '' : $new_raw ); $v->save(); $changed_any = true; } if ($changed_any) { wc_delete_product_transients($product_id); // عمداً $product->save(); نداریم تا تاریخ پدر بی‌دلیل آپدیت نشه } wp_safe_redirect( get_permalink($product_id) . '?fpe_saved=1' ); exit; } wp_die('Unsupported product type'); }); /** Toast */ add_action('wp_footer', function () { if ( ! function_exists('is_product') || ! is_product() ) return; if ( isset($_GET['fpe_saved']) ) { echo '<div id="fpe-toast" style="position:fixed;bottom:18px;left:18px;z-index:999999;background:#111;color:#fff;padding:10px 12px;border-radius:14px;font-size:13px;">به‌روزرسانی انجام شد ✅</div>'; echo '<script>setTimeout(function(){var t=document.getElementById("fpe-toast"); if(t) t.remove();}, 3200);</script>'; } });
/**
 * Front-end Price Editor (Simple + Variable) - Code Snippets
 * ✅ فقط قیمت عادی (Regular) — بدون فروش ویژه
 * ✅ نمایش باکس جمع/بازشو + ریسپانسیو موبایل + رنگ‌بندی اکسل
 * ✅ فرمت سه‌تایی حین تایپ (23,000,000) ولی ذخیره امن (فقط ارقام)
 * ✅ فقط همان تنوعی که واقعاً تغییر کرده save می‌شود (تاریخ بقیه تنوع‌ها آپدیت نمی‌شود)
 * ✅ دکمه مثل وردپرس: «به‌روزرسانی»
 *
 * نصب: Code Snippets → Add New → PHP → Run everywhere → فعال
 */

if ( ! defined('ABSPATH') ) exit;

/** Optional: remove "choose an option" placeholder in variation dropdowns */
add_filter('woocommerce_dropdown_variation_attribute_options_args', function($args){
    $args['show_option_none'] = false;
    return $args;
});

/** Convert Persian/Arabic digits to English + keep only digits */
function fpe_digits_only($val){
    $val = (string) $val;
    $map = [
        '۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9',
        '٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9',
    ];
    $val = strtr($val, $map);
    return preg_replace('/\D+/', '', $val);
}

/** Variation label helpers */
function fpe_attribute_label($attr_key, $parent_product){
    $key = preg_replace('/^attribute_/', '', (string)$attr_key);

    if (strpos($key, 'pa_') === 0 && taxonomy_exists($key)) {
        $tax = get_taxonomy($key);
        if ($tax && ! empty($tax->labels->singular_name)) return $tax->labels->singular_name;
        return wc_attribute_label($key, $parent_product);
    }

    $label = wc_attribute_label($key, $parent_product);
    if ($label && $label !== $key) return $label;

    return str_replace(['pa_', '-', '_'], ['', ' ', ' '], $key);
}

function fpe_attribute_value_readable($taxonomy_or_name, $raw_val){
    $raw_val = (string)$raw_val;
    $decoded = rawurldecode($raw_val);
    $tax = preg_replace('/^attribute_/', '', (string)$taxonomy_or_name);

    if (strpos($tax, 'pa_') === 0 && taxonomy_exists($tax)) {
        $term = get_term_by('slug', $raw_val, $tax);
        if ( ! $term || is_wp_error($term) ) $term = get_term_by('slug', $decoded, $tax);
        if ( ! $term || is_wp_error($term) ) $term = get_term_by('name', $decoded, $tax);
        if ( $term && ! is_wp_error($term) ) return $term->name;
        return $decoded;
    }

    return $decoded;
}

function fpe_get_variation_label($variation, $parent_product){
    $out = [];
    foreach ((array)$variation->get_attributes() as $k => $v) {
        if ($v === '' || $v === null) continue;
        $out[] = fpe_attribute_label($k, $parent_product) . ': ' . fpe_attribute_value_readable($k, $v);
    }
    return $out ? implode(' | ', $out) : ('تنوع #' . $variation->get_id());
}

/** UI */
add_action('woocommerce_after_add_to_cart_form', function () {

    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( ! is_user_logged_in() ) return;
    if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) return;

    global $product;
    if ( ! $product ) return;

    $type = $product->get_type();
    if ( $type !== 'simple' && $type !== 'variable' ) return;

    $product_id = $product->get_id();
    $nonce = wp_create_nonce('fpe_save_prices');
    $title = ($type === 'simple') ? 'ویرایش قیمت محصول (ساده)' : 'ویرایش قیمت تنوع‌ها (متغیر)';

    echo '<style>
    .fpe-wrap{margin:16px 0;}
    .fpe-details{border:1px solid #e5e7eb;border-radius:14px;background:#fafafa;overflow:hidden;}
    .fpe-details>summary{list-style:none;cursor:pointer;padding:12px;display:flex;align-items:center;gap:10px;user-select:none;}
    .fpe-details>summary::-webkit-details-marker{display:none;}
    .fpe-badge{font-size:12px;padding:4px 10px;border-radius:999px;background:#111;color:#fff;white-space:nowrap;}
    .fpe-title{font-size:14px;font-weight:900;line-height:1.4;margin:0;flex:1;}
    .fpe-hint{font-size:12px;opacity:.7;margin:0;}
    .fpe-body{padding:12px;}

    .fpe-grid-head{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:10px;border-bottom:1px solid #e5e7eb;font-size:13px;font-weight:900;background:#f3f4f6;border-radius:12px;margin-bottom:10px;}
    .fpe-row{display:grid;grid-template-columns:1fr 240px;gap:10px;padding:12px 10px;border:1px solid #e5e7eb;border-radius:14px;align-items:center;margin-bottom:10px;background:#fff;}
    .fpe-rows .fpe-row:nth-child(even){ background:#eef6ff; }

    .fpe-attr{font-size:13px;line-height:1.6;word-break:break-word;font-weight:800;}

    .fpe-input{
      width:100%;padding:10px;border:1px solid #c3c4c7;border-radius:10px;font-size:16px;outline:none;background:#fff;
      direction:ltr;text-align:center;
    }
    .fpe-input:focus{border-color:#2271b1; box-shadow:0 0 0 1px #2271b1;}

    .fpe-actions{margin-top:12px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;}
    .fpe-note{font-size:12px;opacity:.75;margin:0;}

    .fpe-btn{width:100%;padding:12px 18px;border:1px solid #2271b1;border-radius:6px;cursor:pointer;font-size:14px;font-weight:700;background:#2271b1;color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.08);}
    .fpe-btn:hover{background:#135e96;border-color:#135e96;}
    .fpe-btn:active{background:#0a4b78;border-color:#0a4b78;transform:translateY(1px);}

    @media (max-width:680px){
      .fpe-grid-head{display:none;}
      .fpe-row{grid-template-columns:1fr;gap:10px;padding:12px;}
      .fpe-field{display:flex;flex-direction:column;gap:6px;}
      .fpe-label{font-size:12px;opacity:.75;}
      .fpe-attr{font-size:14px;}
    }
    @media (min-width:681px){
      .fpe-btn{width:auto;min-width:220px;}
      .fpe-label{display:none;}
      .fpe-field{display:block;}
    }
    </style>';

    echo '<div class="fpe-wrap">';
    echo '<details class="fpe-details" '.(isset($_GET["fpe_saved"]) ? "open" : "").'>';
    echo '<summary><span class="fpe-badge">مدیر</span>
            <div style="min-width:0;">
              <p class="fpe-title">'.esc_html($title).'</p>
              <p class="fpe-hint">قیمت‌ها حین تایپ سه‌تایی جدا می‌شوند ✅</p>
            </div>
            <span style="opacity:.65;font-size:18px;">⌄</span>
          </summary>';

    echo '<div class="fpe-body"><form method="post" id="fpe-form">';
    echo '<input type="hidden" name="fpe_product_id" value="'.esc_attr($product_id).'">';
    echo '<input type="hidden" name="fpe_nonce" value="'.esc_attr($nonce).'">';
    echo '<input type="hidden" name="fpe_type" value="'.esc_attr($type).'">';

    echo '<div class="fpe-grid-head"><div>'.($type==='simple'?'محصول':'تنوع').'</div><div>قیمت</div></div>';
    echo '<div class="fpe-rows">';

    if ($type === 'simple') {
        $raw = fpe_digits_only($product->get_regular_price());
        echo '<div class="fpe-row">
                <div class="fpe-attr">این محصول</div>
                <div class="fpe-field">
                  <div class="fpe-label">قیمت</div>
                  <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_simple_regular" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000">
                </div>
              </div>';
    } else {
        foreach ($product->get_children() as $variation_id) {
            $v = wc_get_product($variation_id);
            if (!$v) continue;

            $label = fpe_get_variation_label($v, $product);
            $raw = fpe_digits_only($v->get_regular_price());

            echo '<div class="fpe-row">
                    <div class="fpe-attr">'.esc_html($label).'</div>
                    <div class="fpe-field">
                      <div class="fpe-label">قیمت</div>
                      <input class="fpe-input fpe-price" type="text" inputmode="numeric" name="fpe_regular['.esc_attr($variation_id).']" value="'.esc_attr($raw).'" placeholder="مثلاً 23000000">
                    </div>
                  </div>';
        }
    }

    echo '</div>';

    echo '<div class="fpe-actions">
            <button class="fpe-btn" type="submit" name="fpe_save" value="1">به‌روزرسانی</button>
            <p class="fpe-note">بعد از به‌روزرسانی، صفحه رفرش می‌شود.</p>
          </div>';

    echo '</form></div></details></div>';

    // JS: format while typing + keep caret position + submit digits-only
    echo '<script>
    (function(){
      function toEnDigits(s){
        if(!s) return "";
        var map = {"۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9"};
        return String(s).replace(/[۰-۹٠-٩]/g, function(ch){ return map[ch] || ch; });
      }
      function digitsOnly(s){ return toEnDigits(s).replace(/\\D+/g,""); }
      function format3(s){
        s = digitsOnly(s);
        if(!s) return "";
        return s.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ",");
      }
      function caretDigitsIndex(value, caretPos){
        var left = value.slice(0, caretPos);
        return digitsOnly(left).length;
      }
      function caretFromDigitsIndex(formatted, digitIndex){
        var count = 0;
        for (var i=0; i<formatted.length; i++){
          if (/\\d/.test(formatted[i])) count++;
          if (count >= digitIndex) return i+1;
        }
        return formatted.length;
      }

      var inputs = document.querySelectorAll(".fpe-wrap .fpe-price");
      inputs.forEach(function(inp){
        inp.value = format3(inp.value);

        inp.addEventListener("input", function(){
          var oldVal = inp.value;
          var caret = inp.selectionStart || 0;

          var dIndex = caretDigitsIndex(oldVal, caret);
          var newVal = format3(oldVal);

          inp.value = newVal;

          var newCaret = caretFromDigitsIndex(newVal, dIndex);
          try { inp.setSelectionRange(newCaret, newCaret); } catch(err){}
        });

        inp.addEventListener("paste", function(){
          setTimeout(function(){ inp.value = format3(inp.value); }, 0);
        });
      });

      var form = document.getElementById("fpe-form");
      if(form){
        form.addEventListener("submit", function(){
          inputs.forEach(function(inp){ inp.value = digitsOnly(inp.value); });
        });
      }
    })();
    </script>';

}, 50);

/** Save handler (ONLY regular price) */
add_action('template_redirect', function () {

    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( empty($_POST['fpe_save']) ) return;

    if ( ! is_user_logged_in() ) wp_die('Access denied');
    if ( ! current_user_can('manage_woocommerce') && ! current_user_can('administrator') ) wp_die('Access denied');

    $nonce = isset($_POST['fpe_nonce']) ? sanitize_text_field($_POST['fpe_nonce']) : '';
    if ( ! wp_verify_nonce($nonce, 'fpe_save_prices') ) wp_die('Security check failed');

    $product_id = isset($_POST['fpe_product_id']) ? absint($_POST['fpe_product_id']) : 0;
    if ( ! $product_id ) wp_die('Invalid product');

    $product = wc_get_product($product_id);
    if ( ! $product ) wp_die('Product not found');

    $type = isset($_POST['fpe_type']) ? sanitize_text_field($_POST['fpe_type']) : $product->get_type();

    // SIMPLE
    if ( $type === 'simple' && $product->is_type('simple') ) {
        $new_raw = isset($_POST['fpe_simple_regular']) ? fpe_digits_only(wp_unslash($_POST['fpe_simple_regular'])) : '';
        $old_raw = fpe_digits_only($product->get_regular_price());

        if ($new_raw !== $old_raw) {
            $product->set_regular_price( $new_raw === '' ? '' : $new_raw );
            $product->save();
            wc_delete_product_transients($product_id);
        }

        wp_safe_redirect( get_permalink($product_id) . '?fpe_saved=1' );
        exit;
    }

    // VARIABLE (✅ فقط variation های تغییر کرده save می‌شوند)
    if ( $type === 'variable' && $product->is_type('variable') ) {
        $regulars = (isset($_POST['fpe_regular']) && is_array($_POST['fpe_regular'])) ? $_POST['fpe_regular'] : [];
        $changed_any = false;

        foreach ( $product->get_children() as $variation_id ) {
            if ( ! array_key_exists($variation_id, $regulars) ) continue;

            $v = wc_get_product($variation_id);
            if (!$v) continue;

            $new_raw = fpe_digits_only( wp_unslash($regulars[$variation_id]) );
            $old_raw = fpe_digits_only( $v->get_regular_price() );

            // ✅ اگر تغییری نکرده، ذخیره نکن
            if ($new_raw === $old_raw) continue;

            $v->set_regular_price( $new_raw === '' ? '' : $new_raw );
            $v->save();
            $changed_any = true;
        }

        if ($changed_any) {
            wc_delete_product_transients($product_id);
            // عمداً $product->save(); نداریم تا تاریخ پدر بی‌دلیل آپدیت نشه
        }

        wp_safe_redirect( get_permalink($product_id) . '?fpe_saved=1' );
        exit;
    }

    wp_die('Unsupported product type');
});

/** Toast */
add_action('wp_footer', function () {
    if ( ! function_exists('is_product') || ! is_product() ) return;
    if ( isset($_GET['fpe_saved']) ) {
        echo '<div id="fpe-toast" style="position:fixed;bottom:18px;left:18px;z-index:999999;background:#111;color:#fff;padding:10px 12px;border-radius:14px;font-size:13px;">به‌روزرسانی انجام شد ✅</div>';
        echo '<script>setTimeout(function(){var t=document.getElementById("fpe-toast"); if(t) t.remove();}, 3200);</script>';
    }
});
اصلاحی
TEXT - 2026-05-16 22:44:07
add_action('woocommerce_after_add_to_cart_form','mm_product_faq_box',20); function mm_product_faq_box(){ if (!function_exists('is_product') || !is_product()) return; global $product; if (!$product || !is_a($product,'WC_Product')) return; $colors = mm_get_attr_by_keyword($product, array('رنگ','color','colour')); $material = mm_get_attr_by_keyword($product, array('جنس','متریال','material')); $size = mm_get_attr_by_keyword($product, array('سایز','اندازه','ابعاد','size','dimension')); echo '<div class="mm-product-faq" dir="rtl">'; echo '<h3 class="mm-product-faq-title">سوالات پرتکرار</h3>'; echo '<details class="mm-product-faq-item" open>'; echo '<summary class="mm-product-faq-question">آیا امکان بازدید حضوری وجود دارد؟</summary>'; echo '<div class="mm-product-faq-answer">خیر، ما فروشگاه اینترنتی هستیم و امکان بازدید حضوری نداریم. اما می‌توانید داخل پیام‌رسان‌ها به ما پیام بدهید تا تصاویر و نمونه‌های تولید شده محصول برای شما ارسال شود.</div>'; echo '</details>'; if (!empty($colors)) { echo '<details class="mm-product-faq-item">'; echo '<summary class="mm-product-faq-question">این محصول در چه رنگ‌هایی قابل سفارش است؟</summary>'; echo '<div class="mm-product-faq-answer">این محصول در رنگ‌های ' . esc_html($colors) . ' قابل سفارش است.</div>'; echo '</details>'; } if (!empty($material)) { echo '<details class="mm-product-faq-item">'; echo '<summary class="mm-product-faq-question">جنس این محصول چیست؟</summary>'; echo '<div class="mm-product-faq-answer">جنس این محصول ' . esc_html($material) . ' است.</div>'; echo '</details>'; } if (!empty($size)) { echo '<details class="mm-product-faq-item">'; echo '<summary class="mm-product-faq-question">ابعاد یا سایز این محصول چقدر است؟</summary>'; echo '<div class="mm-product-faq-answer">ابعاد/سایز این محصول: ' . esc_html($size) . ' است.</div>'; echo '</details>'; } elseif ($product->has_dimensions()) { echo '<details class="mm-product-faq-item">'; echo '<summary class="mm-product-faq-question">ابعاد این محصول چقدر است؟</summary>'; echo '<div class="mm-product-faq-answer">ابعاد این محصول ' . esc_html(wc_format_dimensions($product->get_dimensions(false))) . ' است.</div>'; echo '</details>'; } echo '<details class="mm-product-faq-item">'; echo '<summary class="mm-product-faq-question">زمان تحویل سفارش چقدر است؟</summary>'; echo '<div class="mm-product-faq-answer">زمان آماده‌سازی و تحویل سفارش بسته به مدل، رنگ و تعداد سفارش متفاوت است. برای اطلاع دقیق از زمان تحویل، قبل از ثبت سفارش می‌توانید از طریق پیام‌رسان‌ها با ما در ارتباط باشید.</div>'; echo '</details>'; echo '<details class="mm-product-faq-item">'; echo '<summary class="mm-product-faq-question">هزینه ارسال چگونه محاسبه می‌شود؟</summary>'; echo '<div class="mm-product-faq-answer">' . mm_get_shipping_faq_text($product) . '</div>'; echo '</details>'; echo '<details class="mm-product-faq-item">'; echo '<summary class="mm-product-faq-question">آیا رنگ محصول دقیقاً مشابه عکس است؟</summary>'; echo '<div class="mm-product-faq-answer">رنگ محصول ممکن است به دلیل نورپردازی هنگام عکاسی یا تفاوت نمایشگر موبایل و مانیتور، کمی با تصویر متفاوت باشد. در صورت نیاز می‌توانید در پیام‌رسان‌ها درخواست نمونه تصویر واقعی تولید شده را ارسال کنید.</div>'; echo '</details>'; echo '<details class="mm-product-faq-item">'; echo '<summary class="mm-product-faq-question">آیا امکان ارسال تصویر نمونه تولید شده وجود دارد؟</summary>'; echo '<div class="mm-product-faq-answer">بله، در صورت موجود بودن نمونه تولید شده، می‌توانید در پیام‌رسان‌ها پیام بدهید تا تصویر یا ویدئوی نمونه برای شما ارسال شود.</div>'; echo '</details>'; echo '<details class="mm-product-faq-item">'; echo '<summary class="mm-product-faq-question">نحوه ثبت سفارش چگونه است؟</summary>'; echo '<div class="mm-product-faq-answer">برای ثبت سفارش، رنگ یا مدل مورد نظر را انتخاب کرده و محصول را به سبد خرید اضافه کنید. سپس اطلاعات ارسال را وارد کرده و سفارش خود را نهایی کنید.</div>'; echo '</details>'; echo '</div>'; echo '<style> .mm-product-faq{ margin:25px 0; padding:20px; border:1px solid #e5e5e5; border-radius:14px; background:#fff; direction:rtl; text-align:right; clear:both; } .mm-product-faq-title{ margin:0 0 16px; font-size:22px; font-weight:700; color:#222; } .mm-product-faq-item{ border:1px solid #eee; border-radius:10px; background:#fafafa; margin-bottom:10px; overflow:hidden; } .mm-product-faq-question{ cursor:pointer; padding:14px 16px; font-weight:700; color:#222; list-style:none; position:relative; } .mm-product-faq-question::-webkit-details-marker{ display:none; } .mm-product-faq-question:after{ content:"+"; position:absolute; left:16px; top:14px; font-size:20px; line-height:1; } .mm-product-faq-item[open] .mm-product-faq-question:after{ content:"−"; } .mm-product-faq-answer{ padding:0 16px 14px; color:#555; line-height:2; font-size:15px; } </style>'; } function mm_get_attr_by_keyword($product, $keywords = array()){ $values = array(); if (!$product || empty($keywords)) return ''; $attributes = $product->get_attributes(); foreach ($attributes as $attribute) { if (!is_a($attribute, 'WC_Product_Attribute
add_action('woocommerce_after_add_to_cart_form','mm_product_faq_box',20);

function mm_product_faq_box(){
    if (!function_exists('is_product') || !is_product()) return;

    global $product;
    if (!$product || !is_a($product,'WC_Product')) return;

    $colors   = mm_get_attr_by_keyword($product, array('رنگ','color','colour'));
    $material = mm_get_attr_by_keyword($product, array('جنس','متریال','material'));
    $size     = mm_get_attr_by_keyword($product, array('سایز','اندازه','ابعاد','size','dimension'));

    echo '<div class="mm-product-faq" dir="rtl">';
    echo '<h3 class="mm-product-faq-title">سوالات پرتکرار</h3>';

    echo '<details class="mm-product-faq-item" open>';
    echo '<summary class="mm-product-faq-question">آیا امکان بازدید حضوری وجود دارد؟</summary>';
    echo '<div class="mm-product-faq-answer">خیر، ما فروشگاه اینترنتی هستیم و امکان بازدید حضوری نداریم. اما می‌توانید داخل پیام‌رسان‌ها به ما پیام بدهید تا تصاویر و نمونه‌های تولید شده محصول برای شما ارسال شود.</div>';
    echo '</details>';

    if (!empty($colors)) {
        echo '<details class="mm-product-faq-item">';
        echo '<summary class="mm-product-faq-question">این محصول در چه رنگ‌هایی قابل سفارش است؟</summary>';
        echo '<div class="mm-product-faq-answer">این محصول در رنگ‌های ' . esc_html($colors) . ' قابل سفارش است.</div>';
        echo '</details>';
    }

    if (!empty($material)) {
        echo '<details class="mm-product-faq-item">';
        echo '<summary class="mm-product-faq-question">جنس این محصول چیست؟</summary>';
        echo '<div class="mm-product-faq-answer">جنس این محصول ' . esc_html($material) . ' است.</div>';
        echo '</details>';
    }

    if (!empty($size)) {
        echo '<details class="mm-product-faq-item">';
        echo '<summary class="mm-product-faq-question">ابعاد یا سایز این محصول چقدر است؟</summary>';
        echo '<div class="mm-product-faq-answer">ابعاد/سایز این محصول: ' . esc_html($size) . ' است.</div>';
        echo '</details>';
    } elseif ($product->has_dimensions()) {
        echo '<details class="mm-product-faq-item">';
        echo '<summary class="mm-product-faq-question">ابعاد این محصول چقدر است؟</summary>';
        echo '<div class="mm-product-faq-answer">ابعاد این محصول ' . esc_html(wc_format_dimensions($product->get_dimensions(false))) . ' است.</div>';
        echo '</details>';
    }

    echo '<details class="mm-product-faq-item">';
    echo '<summary class="mm-product-faq-question">زمان تحویل سفارش چقدر است؟</summary>';
    echo '<div class="mm-product-faq-answer">زمان آماده‌سازی و تحویل سفارش بسته به مدل، رنگ و تعداد سفارش متفاوت است. برای اطلاع دقیق از زمان تحویل، قبل از ثبت سفارش می‌توانید از طریق پیام‌رسان‌ها با ما در ارتباط باشید.</div>';
    echo '</details>';

    echo '<details class="mm-product-faq-item">';
    echo '<summary class="mm-product-faq-question">هزینه ارسال چگونه محاسبه می‌شود؟</summary>';
    echo '<div class="mm-product-faq-answer">' . mm_get_shipping_faq_text($product) . '</div>';
    echo '</details>';

    echo '<details class="mm-product-faq-item">';
    echo '<summary class="mm-product-faq-question">آیا رنگ محصول دقیقاً مشابه عکس است؟</summary>';
    echo '<div class="mm-product-faq-answer">رنگ محصول ممکن است به دلیل نورپردازی هنگام عکاسی یا تفاوت نمایشگر موبایل و مانیتور، کمی با تصویر متفاوت باشد. در صورت نیاز می‌توانید در پیام‌رسان‌ها درخواست نمونه تصویر واقعی تولید شده را ارسال کنید.</div>';
    echo '</details>';

    echo '<details class="mm-product-faq-item">';
    echo '<summary class="mm-product-faq-question">آیا امکان ارسال تصویر نمونه تولید شده وجود دارد؟</summary>';
    echo '<div class="mm-product-faq-answer">بله، در صورت موجود بودن نمونه تولید شده، می‌توانید در پیام‌رسان‌ها پیام بدهید تا تصویر یا ویدئوی نمونه برای شما ارسال شود.</div>';
    echo '</details>';

    echo '<details class="mm-product-faq-item">';
    echo '<summary class="mm-product-faq-question">نحوه ثبت سفارش چگونه است؟</summary>';
    echo '<div class="mm-product-faq-answer">برای ثبت سفارش، رنگ یا مدل مورد نظر را انتخاب کرده و محصول را به سبد خرید اضافه کنید. سپس اطلاعات ارسال را وارد کرده و سفارش خود را نهایی کنید.</div>';
    echo '</details>';

    echo '</div>';

    echo '<style>
        .mm-product-faq{
            margin:25px 0;
            padding:20px;
            border:1px solid #e5e5e5;
            border-radius:14px;
            background:#fff;
            direction:rtl;
            text-align:right;
            clear:both;
        }
        .mm-product-faq-title{
            margin:0 0 16px;
            font-size:22px;
            font-weight:700;
            color:#222;
        }
        .mm-product-faq-item{
            border:1px solid #eee;
            border-radius:10px;
            background:#fafafa;
            margin-bottom:10px;
            overflow:hidden;
        }
        .mm-product-faq-question{
            cursor:pointer;
            padding:14px 16px;
            font-weight:700;
            color:#222;
            list-style:none;
            position:relative;
        }
        .mm-product-faq-question::-webkit-details-marker{
            display:none;
        }
        .mm-product-faq-question:after{
            content:"+";
            position:absolute;
            left:16px;
            top:14px;
            font-size:20px;
            line-height:1;
        }
        .mm-product-faq-item[open] .mm-product-faq-question:after{
            content:"−";
        }
        .mm-product-faq-answer{
            padding:0 16px 14px;
            color:#555;
            line-height:2;
            font-size:15px;
        }
    </style>';
}

function mm_get_attr_by_keyword($product, $keywords = array()){
    $values = array();

    if (!$product || empty($keywords)) return '';

    $attributes = $product->get_attributes();

    foreach ($attributes as $attribute) {
        if (!is_a($attribute, 'WC_Product_Attribute
توض
TEXT - 2026-05-16 22:21:31
add_action('woocommerce_after_add_to_cart_form','mm_auto_product_faq_output',20); function mm_auto_product_faq_output(){ if (!function_exists('is_product') || !is_product()) return; global $product; if (!$product || !is_a($product,'WC_Product')) return; echo mm_auto_product_faq_html($product); } function mm_auto_product_faq_html($product){ $faqs = array(); $colors = mm_get_product_attr_by_keywords($product, array('رنگ','color','colour')); if (!empty($colors)) { $faqs[] = array( 'q' => 'این محصول در چه رنگ‌هایی موجود است؟', 'a' => 'این محصول در رنگ‌های ' . esc_html(implode('، ', $colors)) . ' قابل سفارش است.' ); } $materials = mm_get_product_attr_by_keywords($product, array('جنس','متریال','material')); if (!empty($materials)) { $faqs[] = array( 'q' => 'جنس این محصول چیست؟', 'a' => 'جنس این محصول ' . esc_html(implode('، ', $materials)) . ' است.' ); } $sizes = mm_get_product_attr_by_keywords($product, array('سایز','اندازه','ابعاد','size','dimension')); if (!empty($sizes)) { $faqs[] = array( 'q' => 'ابعاد یا سایز این محصول چیست؟', 'a' => 'ابعاد/سایز این محصول: ' . esc_html(implode('، ', $sizes)) . '.' ); } elseif ($product->has_dimensions()) { $faqs[] = array( 'q' => 'ابعاد این محصول چیست؟', 'a' => 'ابعاد ثبت‌شده این محصول: ' . esc_html(wc_format_dimensions($product->get_dimensions(false))) . '.' ); } if ($product->has_weight()) { $faqs[] = array( 'q' => 'وزن این محصول چقدر است؟', 'a' => 'وزن ثبت‌شده این محصول ' . esc_html(wc_format_weight($product->get_weight())) . ' است.' ); } if ($product->is_in_stock()) { $stock_text = 'بله، این محصول در حال حاضر موجود است.'; if ($product->managing_stock() && $product->get_stock_quantity() !== null) { $stock_text = 'بله، این محصول موجود است و تعداد موجودی فعلی آن ' . esc_html($product->get_stock_quantity()) . ' عدد است.'; } } else { $stock_text = 'خیر، این محصول در حال حاضر ناموجود است.'; } $faqs[] = array( 'q' => 'آیا این محصول موجود است؟', 'a' => $stock_text ); $faqs[] = array( 'q' => 'هزینه ارسال این محصول چگونه محاسبه می‌شود؟', 'a' => mm_get_shipping_text($product) ); $assembly = mm_get_product_attr_by_keywords($product, array('مونتاژ','نصب','assembly','install')); if (!empty($assembly)) { $faqs[] = array( 'q' => 'آیا این محصول نیاز به نصب یا مونتاژ دارد؟', 'a' => esc_html(implode('، ', $assembly)) ); } if (empty($faqs)) return ''; ob_start(); ?> <div class="mm-product-faq-box" dir="rtl"> <h3 class="mm-product-faq-title">سوالات پرتکرار این محصول</h3> <div class="mm-product-faq-items"> <?php foreach($faqs as $i => $faq): ?> <details class="mm-product-faq-item" <?php echo $i === 0 ? 'open' : ''; ?>> <summary class="mm-product-faq-question"><?php echo esc_html($faq['q']); ?></summary> <div class="mm-product-faq-answer"><?php echo wpautop(wp_kses_post($faq['a'])); ?></div> </details> <?php endforeach; ?> </div> </div> <style> .mm-product-faq-box{ margin:25px 0; padding:20px; border:1px solid #e5e5e5; border-radius:14px; background:#fff; text-align:right; direction:rtl; } .mm-product-faq-title{ margin:0 0 16px; font-size:22px; font-weight:700; color:#222; } .mm-product-faq-items{ display:flex; flex-direction:column; gap:10px; } .mm-product-faq-item{ border:1px solid #ececec; border-radius:10px; background:#fafafa; overflow:hidden; } .mm-product-faq-question{ padding:14px 16px; cursor:pointer; font-weight:700; position:relative; list-style:none; } .mm-product-faq-question::-webkit-details-marker{ display:none; } .mm-product-faq-question:before{ content:"+"; position:absolute; left:16px; top:12px; font-size:22px; line-height:1; } .mm-product-faq-item[open] .mm-product-faq-question:before{ content:"−"; } .mm-product-faq-answer{ padding:0 16px 14px; color:#555; line-height:2; font-size:15px; } .mm-product-faq-answer p{ margin:0; } </style> <?php return ob_get_clean(); } function mm_get_product_attr_by_keywords($product, $keywords = array()){ $values = array(); $attributes = $product->get_attributes(); foreach($attributes as $attribute){ if (!is_a($attribute, 'WC_Product_Attribute')) continue; $attr_name = $attribute->get_name(); $attr_label = wc_attribute_label($attr_name); $search_in = mb_strtolower($attr_name . ' ' . $attr_label); $matched = false; foreach($keywords as $keyword){ if (mb_strpos($search_in, mb_strtolower($keyword)) !== false){ $matched = true; break; } } if (!$matched) continue; if ($attribute->is_taxonomy()){ $terms = wc_get_product_terms($product->get_id(), $attr_name, array('fields' => 'names')); if (!empty($terms) && !is_wp_error($terms)){ $values = array_merge($values, $terms); } } else { $options = $attribute->get_options(); if (!empty($options)){ $values = array_merge($values, $options); } } } if ($product->is_type('variable')){ $variation_attributes = $product->get_variation_attributes(); foreach($variation_attributes as $attr_key => $options){ $clean_key = str_replace('attribute_', '', $attr_key); $attr_label = wc_attribute_label($clean_key); $search_in = mb_strtolower($clean_key . ' ' . $attr_label); $matched = false; foreach($keywords as $keyword){ if (mb_strpos($search_in, mb_strtolower($keyword)) !== false){ $matched = true; break; } } if (!$matched) continue; foreach($options as $option){ if (taxonomy_exists($clean_key)){ $term = get_term_by('slug', $option, $clean_key); $values[] = ($term && !is_wp_error($term)) ? $term->name : $option; } else { $values[] = $option; } } } } $values = array_map('trim', $values); $values = array_filter($values); $values = array_unique($values); return $values; } function mm_get_shipping_text($product){ $shipping_class_id = $product->get_shipping_class_id(); if (!$shipping_class_id){ return 'هزینه ارسال این محصول در مرحله ثبت سفارش بر اساس آدرس و روش ارسال محاسبه می‌شود.'; } $term = get_term($shipping_class_id, 'product_shipping_class'); if (!$term || is_wp_error($term)){ return 'هزینه ارسال این محصول در مرحله ثبت سفارش محاسبه می‌شود.'; } $class_slug = $term->slug; $class_name = $term->name; $messages = array( 'free-shipping' => 'ارسال این محصول رایگان است.', 'light' => 'این محصول در دسته کالاهای سبک قرار دارد و هزینه ارسال آن طبق تعرفه ارسال محاسبه می‌شود.', 'medium' => 'هزینه ارسال این محصول طبق تعرفه کالاهای متوسط محاسبه می‌شود.', 'heavy' => 'این محصول حجیم است و هزینه ارسال آن بر اساس شهر مقصد و باربری محاسبه می‌شود.', 'furniture' => 'این محصول در کلاس حمل مبلمان قرار دارد و هزینه حمل آن بر اساس شهر مقصد و باربری محاسبه می‌شود.', ); if (isset($messages[$class_slug])){ return $messages[$class_slug]; } return 'کلاس حمل‌ونقل این محصول «' . esc_html($class_name) . '» است و هزینه ارسال آن بر اساس مقصد محاسبه می‌شود.'; }
add_action('woocommerce_after_add_to_cart_form','mm_auto_product_faq_output',20);

function mm_auto_product_faq_output(){
    if (!function_exists('is_product') || !is_product()) return;

    global $product;
    if (!$product || !is_a($product,'WC_Product')) return;

    echo mm_auto_product_faq_html($product);
}

function mm_auto_product_faq_html($product){
    $faqs = array();

    $colors = mm_get_product_attr_by_keywords($product, array('رنگ','color','colour'));
    if (!empty($colors)) {
        $faqs[] = array(
            'q' => 'این محصول در چه رنگ‌هایی موجود است؟',
            'a' => 'این محصول در رنگ‌های ' . esc_html(implode('، ', $colors)) . ' قابل سفارش است.'
        );
    }

    $materials = mm_get_product_attr_by_keywords($product, array('جنس','متریال','material'));
    if (!empty($materials)) {
        $faqs[] = array(
            'q' => 'جنس این محصول چیست؟',
            'a' => 'جنس این محصول ' . esc_html(implode('، ', $materials)) . ' است.'
        );
    }

    $sizes = mm_get_product_attr_by_keywords($product, array('سایز','اندازه','ابعاد','size','dimension'));
    if (!empty($sizes)) {
        $faqs[] = array(
            'q' => 'ابعاد یا سایز این محصول چیست؟',
            'a' => 'ابعاد/سایز این محصول: ' . esc_html(implode('، ', $sizes)) . '.'
        );
    } elseif ($product->has_dimensions()) {
        $faqs[] = array(
            'q' => 'ابعاد این محصول چیست؟',
            'a' => 'ابعاد ثبت‌شده این محصول: ' . esc_html(wc_format_dimensions($product->get_dimensions(false))) . '.'
        );
    }

    if ($product->has_weight()) {
        $faqs[] = array(
            'q' => 'وزن این محصول چقدر است؟',
            'a' => 'وزن ثبت‌شده این محصول ' . esc_html(wc_format_weight($product->get_weight())) . ' است.'
        );
    }

    if ($product->is_in_stock()) {
        $stock_text = 'بله، این محصول در حال حاضر موجود است.';
        if ($product->managing_stock() && $product->get_stock_quantity() !== null) {
            $stock_text = 'بله، این محصول موجود است و تعداد موجودی فعلی آن ' . esc_html($product->get_stock_quantity()) . ' عدد است.';
        }
    } else {
        $stock_text = 'خیر، این محصول در حال حاضر ناموجود است.';
    }

    $faqs[] = array(
        'q' => 'آیا این محصول موجود است؟',
        'a' => $stock_text
    );

    $faqs[] = array(
        'q' => 'هزینه ارسال این محصول چگونه محاسبه می‌شود؟',
        'a' => mm_get_shipping_text($product)
    );

    $assembly = mm_get_product_attr_by_keywords($product, array('مونتاژ','نصب','assembly','install'));
    if (!empty($assembly)) {
        $faqs[] = array(
            'q' => 'آیا این محصول نیاز به نصب یا مونتاژ دارد؟',
            'a' => esc_html(implode('، ', $assembly))
        );
    }

    if (empty($faqs)) return '';

    ob_start();
    ?>
    <div class="mm-product-faq-box" dir="rtl">
        <h3 class="mm-product-faq-title">سوالات پرتکرار این محصول</h3>
        <div class="mm-product-faq-items">
            <?php foreach($faqs as $i => $faq): ?>
                <details class="mm-product-faq-item" <?php echo $i === 0 ? 'open' : ''; ?>>
                    <summary class="mm-product-faq-question"><?php echo esc_html($faq['q']); ?></summary>
                    <div class="mm-product-faq-answer"><?php echo wpautop(wp_kses_post($faq['a'])); ?></div>
                </details>
            <?php endforeach; ?>
        </div>
    </div>

    <style>
    .mm-product-faq-box{
        margin:25px 0;
        padding:20px;
        border:1px solid #e5e5e5;
        border-radius:14px;
        background:#fff;
        text-align:right;
        direction:rtl;
    }
    .mm-product-faq-title{
        margin:0 0 16px;
        font-size:22px;
        font-weight:700;
        color:#222;
    }
    .mm-product-faq-items{
        display:flex;
        flex-direction:column;
        gap:10px;
    }
    .mm-product-faq-item{
        border:1px solid #ececec;
        border-radius:10px;
        background:#fafafa;
        overflow:hidden;
    }
    .mm-product-faq-question{
        padding:14px 16px;
        cursor:pointer;
        font-weight:700;
        position:relative;
        list-style:none;
    }
    .mm-product-faq-question::-webkit-details-marker{
        display:none;
    }
    .mm-product-faq-question:before{
        content:"+";
        position:absolute;
        left:16px;
        top:12px;
        font-size:22px;
        line-height:1;
    }
    .mm-product-faq-item[open] .mm-product-faq-question:before{
        content:"−";
    }
    .mm-product-faq-answer{
        padding:0 16px 14px;
        color:#555;
        line-height:2;
        font-size:15px;
    }
    .mm-product-faq-answer p{
        margin:0;
    }
    </style>
    <?php
    return ob_get_clean();
}

function mm_get_product_attr_by_keywords($product, $keywords = array()){
    $values = array();
    $attributes = $product->get_attributes();

    foreach($attributes as $attribute){
        if (!is_a($attribute, 'WC_Product_Attribute')) continue;

        $attr_name  = $attribute->get_name();
        $attr_label = wc_attribute_label($attr_name);
        $search_in  = mb_strtolower($attr_name . ' ' . $attr_label);

        $matched = false;
        foreach($keywords as $keyword){
            if (mb_strpos($search_in, mb_strtolower($keyword)) !== false){
                $matched = true;
                break;
            }
        }

        if (!$matched) continue;

        if ($attribute->is_taxonomy()){
            $terms = wc_get_product_terms($product->get_id(), $attr_name, array('fields' => 'names'));
            if (!empty($terms) && !is_wp_error($terms)){
                $values = array_merge($values, $terms);
            }
        } else {
            $options = $attribute->get_options();
            if (!empty($options)){
                $values = array_merge($values, $options);
            }
        }
    }

    if ($product->is_type('variable')){
        $variation_attributes = $product->get_variation_attributes();

        foreach($variation_attributes as $attr_key => $options){
            $clean_key  = str_replace('attribute_', '', $attr_key);
            $attr_label = wc_attribute_label($clean_key);
            $search_in  = mb_strtolower($clean_key . ' ' . $attr_label);

            $matched = false;
            foreach($keywords as $keyword){
                if (mb_strpos($search_in, mb_strtolower($keyword)) !== false){
                    $matched = true;
                    break;
                }
            }

            if (!$matched) continue;

            foreach($options as $option){
                if (taxonomy_exists($clean_key)){
                    $term = get_term_by('slug', $option, $clean_key);
                    $values[] = ($term && !is_wp_error($term)) ? $term->name : $option;
                } else {
                    $values[] = $option;
                }
            }
        }
    }

    $values = array_map('trim', $values);
    $values = array_filter($values);
    $values = array_unique($values);

    return $values;
}

function mm_get_shipping_text($product){
    $shipping_class_id = $product->get_shipping_class_id();

    if (!$shipping_class_id){
        return 'هزینه ارسال این محصول در مرحله ثبت سفارش بر اساس آدرس و روش ارسال محاسبه می‌شود.';
    }

    $term = get_term($shipping_class_id, 'product_shipping_class');

    if (!$term || is_wp_error($term)){
        return 'هزینه ارسال این محصول در مرحله ثبت سفارش محاسبه می‌شود.';
    }

    $class_slug = $term->slug;
    $class_name = $term->name;

    $messages = array(
        'free-shipping' => 'ارسال این محصول رایگان است.',
        'light'         => 'این محصول در دسته کالاهای سبک قرار دارد و هزینه ارسال آن طبق تعرفه ارسال محاسبه می‌شود.',
        'medium'        => 'هزینه ارسال این محصول طبق تعرفه کالاهای متوسط محاسبه می‌شود.',
        'heavy'         => 'این محصول حجیم است و هزینه ارسال آن بر اساس شهر مقصد و باربری محاسبه می‌شود.',
        'furniture'     => 'این محصول در کلاس حمل مبلمان قرار دارد و هزینه حمل آن بر اساس شهر مقصد و باربری محاسبه می‌شود.',
    );

    if (isset($messages[$class_slug])){
        return $messages[$class_slug];
    }

    return 'کلاس حمل‌ونقل این محصول «' . esc_html($class_name) . '» است و هزینه ارسال آن بر اساس مقصد محاسبه می‌شود.';
}
اطلاعات محصول
TEXT - 2026-05-16 22:21:14
<?php /** * Auto Product FAQ for WooCommerce - WPCode Ready * نمایش خودکار سوالات پرتکرار محصول براساس ویژگی‌ها، تنوع‌ها و کلاس حمل‌ونقل */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * تنظیمات اصلی */ function mm_auto_faq_settings() { return array( // اگر true باشد، FAQ خودکار در صفحه محصول نمایش داده می‌شود 'auto_display' => true, // محل نمایش در صفحه محصول // woocommerce_after_single_product_summary = بعد از توضیحات و تب‌ها 'hook' => 'woocommerce_after_single_product_summary', 'priority' => 12, /** * نامک ویژگی‌ها * اگر ویژگی شما در ووکامرس با نام فارسی ساخته شده، ممکن است نامک آن مثلاً pa_rang یا pa_color باشد. * اینجا چند حالت رایج گذاشته شده. اگر نامک شما فرق دارد، اضافه‌اش کنید. */ 'color_attributes' => array( 'pa_color', 'pa_rang', 'pa_رنگ', 'color', 'rang', 'رنگ', ), 'size_attributes' => array( 'pa_size', 'pa_sizes', 'pa_sayz', 'pa_سایز', 'pa_dimensions', 'pa_abandaze', 'pa_ابعاد', 'size', 'سایز', 'dimensions', 'ابعاد', ), 'material_attributes' => array( 'pa_material', 'pa_jens', 'pa_جنس', 'material', 'jens', 'جنس', ), 'assembly_attributes' => array( 'pa_assembly', 'pa_montage', 'pa_مونتاژ', 'assembly', 'montage', 'مونتاژ', 'نیاز-به-نصب', 'pa_niyaz-be-nasb', ), /** * متن هزینه حمل بر اساس کلاس حمل‌ونقل * نکته مهم: * خود ووکامرس در کلاس حمل‌ونقل، مبلغ ثابت ذخیره نمی‌کند. * مبلغ حمل معمولاً داخل Shipping Zone و روش ارسال تعریف می‌شود. * بنابراین برای نمایش مبلغ دقیق، اینجا کلاس حمل را به متن یا مبلغ وصل می‌کنیم. * * کلیدها باید نامک Shipping Class باشند. * مثال: * 'heavy' => 'هزینه ارسال این محصول به‌صورت پس‌کرایه یا باربری محاسبه می‌شود.' */ 'shipping_class_messages' => array( // نمونه‌ها - مطابق کلاس‌های سایت خودت تغییر بده 'free-shipping' => 'ارسال این محصول رایگان است.', 'light' => 'هزینه ارسال این محصول سبک، طبق تعرفه پستی/تیپاکس محاسبه می‌شود.', 'medium' => 'هزینه ارسال این محصول طبق تعرفه حمل کالاهای متوسط محاسبه می‌شود.', 'heavy' => 'این محصول حجیم است و هزینه ارسال آن توسط باربری یا هماهنگی پشتیبانی اعلام می‌شود.', 'furniture' => 'هزینه حمل این محصول به دلیل ابعاد مبلمان، براساس شهر مقصد و باربری محاسبه می‌شود.', ), /** * اگر خواستی مبلغ ثابت برای کلاس حمل نمایش بدهی، اینجا فعال کن. * مبلغ‌ها به تومان/واحد پول فروشگاه طبق تنظیمات ووکامرس نمایش داده می‌شوند. */ 'shipping_class_prices' => array( // مثال: // 'light' => 150000, // 'medium' => 250000, // 'heavy' => 450000, // 'furniture' => 650000, ), ); } /** * گرفتن مقدار ویژگی محصول با چند نامک احتمالی */ function mm_get_product_attribute_values( $product, $possible_slugs = array() ) { if ( ! $product || empty( $possible_slugs ) ) { return array(); } $values = array(); foreach ( $possible_slugs as $slug ) { $slug = sanitize_title( $slug ); // حالت ویژگی عمومی ووکامرس مثل pa_color if ( taxonomy_exists( $slug ) ) { $terms = wc_get_product_terms( $product->get_id(), $slug, array( 'fields' => 'names' ) ); if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) { $values = array_merge( $values, $terms ); } } // حالت ویژگی سفارشی داخل خود محصول $attr_value = $product->get_attribute( $slug ); if ( ! empty( $attr_value ) ) { $parts = array_map( 'trim', explode( '|', $attr_value ) ); $values = array_merge( $values, $parts ); } } // اگر محصول متغیر است، از variation attributes هم بخوان if ( $product->is_type( 'variable' ) ) { $variation_attributes = $product->get_variation_attributes(); foreach ( $variation_attributes as $attr_key => $attr_options ) { $clean_key = str_replace( 'attribute_', '', $attr_key ); $clean_key = sanitize_title( $clean_key ); foreach ( $possible_slugs as $slug ) { $slug = sanitize_title( $slug ); if ( $clean_key === $slug || str_replace( 'pa_', '', $clean_key ) === str_replace( 'pa_', '', $slug ) ) { foreach ( $attr_options as $option ) { if ( taxonomy_exists( $clean_key ) ) { $term = get_term_by( 'slug', $option, $clean_key ); if ( $term && ! is_wp_error( $term ) ) { $values[] = $term->name; } else { $values[] = $option; } } else { $values[] = $option; } } } } } } $values = array_filter( array_map( 'trim', $values ) ); $values = array_unique( $values ); return $values; } /** * ساخت متن خوانا از آرایه */ function mm_human_list( $items ) { $items = array_filter( array_unique( array_map( 'trim', (array) $items ) ) ); if ( empty( $items ) ) { return ''; } return implode( '، ', $items ); } /** * گرفتن متن حمل‌ونقل */ function mm_get_shipping_faq_text( $product ) { $settings = mm_auto_faq_settings(); if ( ! $product ) { return ''; } $shipping_class_id = $product->get_shipping_class_id(); $shipping_class = $product->get_shipping_class(); if ( ! $shipping_class_id || empty( $shipping_class ) ) { return 'هزینه ارسال این محصول در مرحله ثبت سفارش و براساس آدرس و روش ارسال محاسبه می‌شود.'; } $term = get_term( $shipping_class_id, 'product_shipping_class' ); $class_name = ''; $class_slug = $shipping_class; if ( $term && ! is_wp_error( $term ) ) { $class_name = $term->name; $class_slug = $term->slug; } // اگر برای کلاس حمل مبلغ ثابت تعریف شده باشد if ( isset( $settings['shipping_class_prices'][ $class_slug ] ) && $settings['shipping_class_prices'][ $class_slug ] !== '' ) { $price = floatval( $settings['shipping_class_prices'][ $class_slug ] ); return 'کلاس حمل‌ونقل این محصول «' . esc_html( $class_name ) . '» است و هزینه ارسال آن حدوداً ' . wp_kses_post( wc_price( $price ) ) . ' می‌باشد.'; } // اگر برای کلاس حمل پیام اختصاصی تعریف شده باشد if ( isset( $settings['shipping_class_messages'][ $class_slug ] ) && ! empty( $settings['shipping_class_messages'][ $class_slug ] ) ) { return esc_html( $settings['shipping_class_messages'][ $class_slug ] ); } if ( ! empty( $class_name ) ) { return 'کلاس حمل‌ونقل این محصول «' . esc_html( $class_name ) . '» است و هزینه ارسال براساس شهر مقصد و روش ارسال محاسبه می‌شود.'; } return 'هزینه ارسال این محصول در مرحله ثبت سفارش محاسبه می‌شود.'; } /** * تولید FAQ محصول */ function mm_generate_auto_product_faq_html( $product_id = 0 ) { if ( ! function_exists( 'wc_get_product' ) ) { return ''; } if ( ! $product_id ) { global $product; if ( $product && is_a( $product, 'WC_Product' ) ) { $product_id = $product->get_id(); } } $product = wc_get_product( $product_id ); if ( ! $product ) { return ''; } $settings = mm_auto_faq_settings(); $faq_items = array(); /** * رنگ‌ها */ $colors = mm_get_product_attribute_values( $product, $settings['color_attributes'] ); if ( ! empty( $colors ) ) { $faq_items[] = array( 'q' => 'این محصول در چه رنگ‌هایی قابل سفارش است؟', 'a' => 'این محصول در رنگ‌های ' . esc_html( mm_human_list( $colors ) ) . ' قابل سفارش است.', ); } /** * سایز / ابعاد از ویژگی‌ها */ $sizes = mm_get_product_attribute_values( $product, $settings['size_attributes'] ); if ( ! empty( $sizes ) ) { $faq_items[] = array( 'q' => 'سایز یا ابعاد این محصول چقدر است؟', 'a' => 'سایز/ابعاد ثبت‌شده برای این محصول: ' . esc_html( mm_human_list( $sizes ) ) . '.', ); } /** * ابعاد استاندارد ووکامرس */ if ( $product->has_dimensions() ) { $faq_items[] = array( 'q' => 'ابعاد بسته یا محصول چقدر است؟', 'a' => 'ابعاد ثبت‌شده محصول: ' . esc_html( wc_format_dimensions( $product->get_dimensions( false ) ) ) . '.', ); } /** * جنس */ $materials = mm_get_product_attribute_values( $product, $settings['material_attributes'] ); if ( ! empty( $materials ) ) { $faq_items[] = array( 'q' => 'جنس این محصول چیست؟', 'a' => 'جنس این محصول: ' . esc_html( mm_human_list( $materials ) ) . '.', ); } /** * وزن */ if ( $product->has_weight() ) { $faq_items[] = array( 'q' => 'وزن این محصول چقدر است؟', 'a' => 'وزن ثبت‌شده برای این محصول ' . esc_html( wc_format_weight( $product->get_weight() ) ) . ' است.', ); } /** * حمل و نقل */ $shipping_text = mm_get_shipping_faq_text( $product ); if ( ! empty( $shipping_text ) ) { $faq_items[] = array( 'q' => 'هزینه ارسال این محصول چقدر است؟', 'a' => $shipping_text, ); } /** * موجودی */ if ( $product->managing_stock() ) { $stock_quantity = $product->get_stock_quantity(); if ( $product->is_in_stock() && $stock_quantity !== null ) { $faq_items[] = array( 'q' => 'آیا این محصول موجود است؟', 'a' => 'بله، این محصول موجود است. تعداد موجودی فعلی: ' . esc_html( $stock_quantity ) . ' عدد.', ); } elseif ( $product->is_in_stock() ) { $faq_items[] = array( 'q' => 'آیا این محصول موجود است؟', 'a' => 'بله، این محصول در حال حاضر موجود است.', ); } else { $faq_items[] = array( 'q' => 'آیا این محصول موجود است؟', 'a' => 'این محصول در حال حاضر ناموجود است.', ); } } else { if ( $product->is_in_stock() ) { $faq_items[] = array( 'q' => 'آیا این محصول موجود است؟', 'a' => 'بله، این محصول در حال حاضر موجود است.', ); } } /** * مونتاژ / نصب */ $assembly = mm_get_product_attribute_values( $product, $settings['assembly_attributes'] ); if ( ! empty( $assembly ) ) { $faq_items[] = array( 'q' => 'آیا این محصول نیاز به نصب یا مونتاژ دارد؟', 'a' => esc_html( mm_human_list( $assembly ) ), ); } /** * اگر هیچ آیتمی نبود، چیزی نمایش نده */ if ( empty( $faq_items ) ) { return ''; } ob_start(); ?> <section class="mm-auto-product-faq" dir="rtl"> <h2 class="mm-auto-product-faq-title">سوالات پرتکرار این محصول</h2> <div class="mm-auto-product-faq-list"> <?php foreach ( $faq_items as $index => $item ) : ?> <details class="mm-auto-product-faq-item" <?php echo $index === 0 ? 'open' : ''; ?>> <summary class="mm-auto-product-faq-question"> <?php echo esc_html( $item['q'] ); ?> </summary> <div class="mm-auto-product-faq-answer"> <?php echo wp_kses_post( wpautop( $item['a'] ) ); ?> </div> </details> <?php endforeach; ?> </div> </section> <style> .mm-auto-product-faq { margin: 35px 0; padding: 22px; background: #fff; border: 1px solid #e8e8e8; border-radius: 14px; direction: rtl; text-align: right; } .mm-auto-product-faq-title { margin: 0 0 18px; font-size: 22px; font-weight: 700; line-height: 1.6; } .mm-auto-product-faq-list { display: grid; gap: 10px; } .mm-auto-product-faq-item { border: 1px solid #eeeeee; border-radius: 10px; background: #fafafa; overflow: hidden; } .mm-auto-product-faq-question { cursor: pointer; padding: 14px 16px; font-weight: 700; color: #222; list-style: none; } .mm-auto-product-faq-question::-webkit-details-marker { display: none; } .mm-auto-product-faq-question:after { content: "+"; float: left; font-size: 20px; line-height: 1; } .mm-auto-product-faq-item[open] .mm-auto-product-faq-question:after { content: "-"; } .mm-auto-product-faq-answer { padding: 0 16px 14px; color: #555; line-height: 2; font-size: 15px; } .mm-auto-product-faq-answer p { margin: 0; } </style> <?php return ob_get_clean(); } /** * نمایش خودکار در صفحه محصول */ function mm_display_auto_product_faq() { if ( ! is_product() ) { return; } echo mm_generate_auto_product_faq_html(); } $mm_settings = mm_auto_faq_settings(); if ( ! empty( $mm_settings['auto_display'] ) ) { add_action( $mm_settings['hook'], 'mm_display_auto_product_faq', intval( $mm_settings['priority'] ) ); } /** * شورت‌کد اختیاری * استفاده: * [auto_product_faq] */ function mm_auto_product_faq_shortcode( $atts ) { $atts = shortcode_atts( array( 'id' => 0, ), $atts, 'auto_product_faq' ); return mm_generate_auto_product_faq_html( intval( $atts['id'] ) ); } add_shortcode( 'auto_product_faq', 'mm_auto_product_faq_shortcode' );
<?php
/**
 * Auto Product FAQ for WooCommerce - WPCode Ready
 * نمایش خودکار سوالات پرتکرار محصول براساس ویژگی‌ها، تنوع‌ها و کلاس حمل‌ونقل
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * تنظیمات اصلی
 */
function mm_auto_faq_settings() {
    return array(
        // اگر true باشد، FAQ خودکار در صفحه محصول نمایش داده می‌شود
        'auto_display' => true,

        // محل نمایش در صفحه محصول
        // woocommerce_after_single_product_summary = بعد از توضیحات و تب‌ها
        'hook'     => 'woocommerce_after_single_product_summary',
        'priority' => 12,

        /**
         * نامک ویژگی‌ها
         * اگر ویژگی شما در ووکامرس با نام فارسی ساخته شده، ممکن است نامک آن مثلاً pa_rang یا pa_color باشد.
         * اینجا چند حالت رایج گذاشته شده. اگر نامک شما فرق دارد، اضافه‌اش کنید.
         */
        'color_attributes' => array(
            'pa_color',
            'pa_rang',
            'pa_رنگ',
            'color',
            'rang',
            'رنگ',
        ),

        'size_attributes' => array(
            'pa_size',
            'pa_sizes',
            'pa_sayz',
            'pa_سایز',
            'pa_dimensions',
            'pa_abandaze',
            'pa_ابعاد',
            'size',
            'سایز',
            'dimensions',
            'ابعاد',
        ),

        'material_attributes' => array(
            'pa_material',
            'pa_jens',
            'pa_جنس',
            'material',
            'jens',
            'جنس',
        ),

        'assembly_attributes' => array(
            'pa_assembly',
            'pa_montage',
            'pa_مونتاژ',
            'assembly',
            'montage',
            'مونتاژ',
            'نیاز-به-نصب',
            'pa_niyaz-be-nasb',
        ),

        /**
         * متن هزینه حمل بر اساس کلاس حمل‌ونقل
         * نکته مهم:
         * خود ووکامرس در کلاس حمل‌ونقل، مبلغ ثابت ذخیره نمی‌کند.
         * مبلغ حمل معمولاً داخل Shipping Zone و روش ارسال تعریف می‌شود.
         * بنابراین برای نمایش مبلغ دقیق، اینجا کلاس حمل را به متن یا مبلغ وصل می‌کنیم.
         *
         * کلیدها باید نامک Shipping Class باشند.
         * مثال:
         * 'heavy' => 'هزینه ارسال این محصول به‌صورت پس‌کرایه یا باربری محاسبه می‌شود.'
         */
        'shipping_class_messages' => array(
            // نمونه‌ها - مطابق کلاس‌های سایت خودت تغییر بده
            'free-shipping' => 'ارسال این محصول رایگان است.',
            'light'         => 'هزینه ارسال این محصول سبک، طبق تعرفه پستی/تیپاکس محاسبه می‌شود.',
            'medium'        => 'هزینه ارسال این محصول طبق تعرفه حمل کالاهای متوسط محاسبه می‌شود.',
            'heavy'         => 'این محصول حجیم است و هزینه ارسال آن توسط باربری یا هماهنگی پشتیبانی اعلام می‌شود.',
            'furniture'     => 'هزینه حمل این محصول به دلیل ابعاد مبلمان، براساس شهر مقصد و باربری محاسبه می‌شود.',
        ),

        /**
         * اگر خواستی مبلغ ثابت برای کلاس حمل نمایش بدهی، اینجا فعال کن.
         * مبلغ‌ها به تومان/واحد پول فروشگاه طبق تنظیمات ووکامرس نمایش داده می‌شوند.
         */
        'shipping_class_prices' => array(
            // مثال:
            // 'light'     => 150000,
            // 'medium'    => 250000,
            // 'heavy'     => 450000,
            // 'furniture' => 650000,
        ),
    );
}

/**
 * گرفتن مقدار ویژگی محصول با چند نامک احتمالی
 */
function mm_get_product_attribute_values( $product, $possible_slugs = array() ) {
    if ( ! $product || empty( $possible_slugs ) ) {
        return array();
    }

    $values = array();

    foreach ( $possible_slugs as $slug ) {
        $slug = sanitize_title( $slug );

        // حالت ویژگی عمومی ووکامرس مثل pa_color
        if ( taxonomy_exists( $slug ) ) {
            $terms = wc_get_product_terms( $product->get_id(), $slug, array( 'fields' => 'names' ) );
            if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
                $values = array_merge( $values, $terms );
            }
        }

        // حالت ویژگی سفارشی داخل خود محصول
        $attr_value = $product->get_attribute( $slug );
        if ( ! empty( $attr_value ) ) {
            $parts = array_map( 'trim', explode( '|', $attr_value ) );
            $values = array_merge( $values, $parts );
        }
    }

    // اگر محصول متغیر است، از variation attributes هم بخوان
    if ( $product->is_type( 'variable' ) ) {
        $variation_attributes = $product->get_variation_attributes();

        foreach ( $variation_attributes as $attr_key => $attr_options ) {
            $clean_key = str_replace( 'attribute_', '', $attr_key );
            $clean_key = sanitize_title( $clean_key );

            foreach ( $possible_slugs as $slug ) {
                $slug = sanitize_title( $slug );

                if ( $clean_key === $slug || str_replace( 'pa_', '', $clean_key ) === str_replace( 'pa_', '', $slug ) ) {
                    foreach ( $attr_options as $option ) {
                        if ( taxonomy_exists( $clean_key ) ) {
                            $term = get_term_by( 'slug', $option, $clean_key );
                            if ( $term && ! is_wp_error( $term ) ) {
                                $values[] = $term->name;
                            } else {
                                $values[] = $option;
                            }
                        } else {
                            $values[] = $option;
                        }
                    }
                }
            }
        }
    }

    $values = array_filter( array_map( 'trim', $values ) );
    $values = array_unique( $values );

    return $values;
}

/**
 * ساخت متن خوانا از آرایه
 */
function mm_human_list( $items ) {
    $items = array_filter( array_unique( array_map( 'trim', (array) $items ) ) );

    if ( empty( $items ) ) {
        return '';
    }

    return implode( '، ', $items );
}

/**
 * گرفتن متن حمل‌ونقل
 */
function mm_get_shipping_faq_text( $product ) {
    $settings = mm_auto_faq_settings();

    if ( ! $product ) {
        return '';
    }

    $shipping_class_id = $product->get_shipping_class_id();
    $shipping_class    = $product->get_shipping_class();

    if ( ! $shipping_class_id || empty( $shipping_class ) ) {
        return 'هزینه ارسال این محصول در مرحله ثبت سفارش و براساس آدرس و روش ارسال محاسبه می‌شود.';
    }

    $term = get_term( $shipping_class_id, 'product_shipping_class' );

    $class_name = '';
    $class_slug = $shipping_class;

    if ( $term && ! is_wp_error( $term ) ) {
        $class_name = $term->name;
        $class_slug = $term->slug;
    }

    // اگر برای کلاس حمل مبلغ ثابت تعریف شده باشد
    if ( isset( $settings['shipping_class_prices'][ $class_slug ] ) && $settings['shipping_class_prices'][ $class_slug ] !== '' ) {
        $price = floatval( $settings['shipping_class_prices'][ $class_slug ] );

        return 'کلاس حمل‌ونقل این محصول «' . esc_html( $class_name ) . '» است و هزینه ارسال آن حدوداً ' . wp_kses_post( wc_price( $price ) ) . ' می‌باشد.';
    }

    // اگر برای کلاس حمل پیام اختصاصی تعریف شده باشد
    if ( isset( $settings['shipping_class_messages'][ $class_slug ] ) && ! empty( $settings['shipping_class_messages'][ $class_slug ] ) ) {
        return esc_html( $settings['shipping_class_messages'][ $class_slug ] );
    }

    if ( ! empty( $class_name ) ) {
        return 'کلاس حمل‌ونقل این محصول «' . esc_html( $class_name ) . '» است و هزینه ارسال براساس شهر مقصد و روش ارسال محاسبه می‌شود.';
    }

    return 'هزینه ارسال این محصول در مرحله ثبت سفارش محاسبه می‌شود.';
}

/**
 * تولید FAQ محصول
 */
function mm_generate_auto_product_faq_html( $product_id = 0 ) {
    if ( ! function_exists( 'wc_get_product' ) ) {
        return '';
    }

    if ( ! $product_id ) {
        global $product;
        if ( $product && is_a( $product, 'WC_Product' ) ) {
            $product_id = $product->get_id();
        }
    }

    $product = wc_get_product( $product_id );

    if ( ! $product ) {
        return '';
    }

    $settings = mm_auto_faq_settings();

    $faq_items = array();

    /**
     * رنگ‌ها
     */
    $colors = mm_get_product_attribute_values( $product, $settings['color_attributes'] );
    if ( ! empty( $colors ) ) {
        $faq_items[] = array(
            'q' => 'این محصول در چه رنگ‌هایی قابل سفارش است؟',
            'a' => 'این محصول در رنگ‌های ' . esc_html( mm_human_list( $colors ) ) . ' قابل سفارش است.',
        );
    }

    /**
     * سایز / ابعاد از ویژگی‌ها
     */
    $sizes = mm_get_product_attribute_values( $product, $settings['size_attributes'] );
    if ( ! empty( $sizes ) ) {
        $faq_items[] = array(
            'q' => 'سایز یا ابعاد این محصول چقدر است؟',
            'a' => 'سایز/ابعاد ثبت‌شده برای این محصول: ' . esc_html( mm_human_list( $sizes ) ) . '.',
        );
    }

    /**
     * ابعاد استاندارد ووکامرس
     */
    if ( $product->has_dimensions() ) {
        $faq_items[] = array(
            'q' => 'ابعاد بسته یا محصول چقدر است؟',
            'a' => 'ابعاد ثبت‌شده محصول: ' . esc_html( wc_format_dimensions( $product->get_dimensions( false ) ) ) . '.',
        );
    }

    /**
     * جنس
     */
    $materials = mm_get_product_attribute_values( $product, $settings['material_attributes'] );
    if ( ! empty( $materials ) ) {
        $faq_items[] = array(
            'q' => 'جنس این محصول چیست؟',
            'a' => 'جنس این محصول: ' . esc_html( mm_human_list( $materials ) ) . '.',
        );
    }

    /**
     * وزن
     */
    if ( $product->has_weight() ) {
        $faq_items[] = array(
            'q' => 'وزن این محصول چقدر است؟',
            'a' => 'وزن ثبت‌شده برای این محصول ' . esc_html( wc_format_weight( $product->get_weight() ) ) . ' است.',
        );
    }

    /**
     * حمل و نقل
     */
    $shipping_text = mm_get_shipping_faq_text( $product );
    if ( ! empty( $shipping_text ) ) {
        $faq_items[] = array(
            'q' => 'هزینه ارسال این محصول چقدر است؟',
            'a' => $shipping_text,
        );
    }

    /**
     * موجودی
     */
    if ( $product->managing_stock() ) {
        $stock_quantity = $product->get_stock_quantity();

        if ( $product->is_in_stock() && $stock_quantity !== null ) {
            $faq_items[] = array(
                'q' => 'آیا این محصول موجود است؟',
                'a' => 'بله، این محصول موجود است. تعداد موجودی فعلی: ' . esc_html( $stock_quantity ) . ' عدد.',
            );
        } elseif ( $product->is_in_stock() ) {
            $faq_items[] = array(
                'q' => 'آیا این محصول موجود است؟',
                'a' => 'بله، این محصول در حال حاضر موجود است.',
            );
        } else {
            $faq_items[] = array(
                'q' => 'آیا این محصول موجود است؟',
                'a' => 'این محصول در حال حاضر ناموجود است.',
            );
        }
    } else {
        if ( $product->is_in_stock() ) {
            $faq_items[] = array(
                'q' => 'آیا این محصول موجود است؟',
                'a' => 'بله، این محصول در حال حاضر موجود است.',
            );
        }
    }

    /**
     * مونتاژ / نصب
     */
    $assembly = mm_get_product_attribute_values( $product, $settings['assembly_attributes'] );
    if ( ! empty( $assembly ) ) {
        $faq_items[] = array(
            'q' => 'آیا این محصول نیاز به نصب یا مونتاژ دارد؟',
            'a' => esc_html( mm_human_list( $assembly ) ),
        );
    }

    /**
     * اگر هیچ آیتمی نبود، چیزی نمایش نده
     */
    if ( empty( $faq_items ) ) {
        return '';
    }

    ob_start();
    ?>

    <section class="mm-auto-product-faq" dir="rtl">
        <h2 class="mm-auto-product-faq-title">سوالات پرتکرار این محصول</h2>

        <div class="mm-auto-product-faq-list">
            <?php foreach ( $faq_items as $index => $item ) : ?>
                <details class="mm-auto-product-faq-item" <?php echo $index === 0 ? 'open' : ''; ?>>
                    <summary class="mm-auto-product-faq-question">
                        <?php echo esc_html( $item['q'] ); ?>
                    </summary>
                    <div class="mm-auto-product-faq-answer">
                        <?php echo wp_kses_post( wpautop( $item['a'] ) ); ?>
                    </div>
                </details>
            <?php endforeach; ?>
        </div>
    </section>

    <style>
        .mm-auto-product-faq {
            margin: 35px 0;
            padding: 22px;
            background: #fff;
            border: 1px solid #e8e8e8;
            border-radius: 14px;
            direction: rtl;
            text-align: right;
        }

        .mm-auto-product-faq-title {
            margin: 0 0 18px;
            font-size: 22px;
            font-weight: 700;
            line-height: 1.6;
        }

        .mm-auto-product-faq-list {
            display: grid;
            gap: 10px;
        }

        .mm-auto-product-faq-item {
            border: 1px solid #eeeeee;
            border-radius: 10px;
            background: #fafafa;
            overflow: hidden;
        }

        .mm-auto-product-faq-question {
            cursor: pointer;
            padding: 14px 16px;
            font-weight: 700;
            color: #222;
            list-style: none;
        }

        .mm-auto-product-faq-question::-webkit-details-marker {
            display: none;
        }

        .mm-auto-product-faq-question:after {
            content: "+";
            float: left;
            font-size: 20px;
            line-height: 1;
        }

        .mm-auto-product-faq-item[open] .mm-auto-product-faq-question:after {
            content: "-";
        }

        .mm-auto-product-faq-answer {
            padding: 0 16px 14px;
            color: #555;
            line-height: 2;
            font-size: 15px;
        }

        .mm-auto-product-faq-answer p {
            margin: 0;
        }
    </style>

    <?php
    return ob_get_clean();
}

/**
 * نمایش خودکار در صفحه محصول
 */
function mm_display_auto_product_faq() {
    if ( ! is_product() ) {
        return;
    }

    echo mm_generate_auto_product_faq_html();
}

$mm_settings = mm_auto_faq_settings();

if ( ! empty( $mm_settings['auto_display'] ) ) {
    add_action(
        $mm_settings['hook'],
        'mm_display_auto_product_faq',
        intval( $mm_settings['priority'] )
    );
}

/**
 * شورت‌کد اختیاری
 * استفاده:
 * [auto_product_faq]
 */
function mm_auto_product_faq_shortcode( $atts ) {
    $atts = shortcode_atts(
        array(
            'id' => 0,
        ),
        $atts,
        'auto_product_faq'
    );

    return mm_generate_auto_product_faq_html( intval( $atts['id'] ) );
}
add_shortcode( 'auto_product_faq', 'mm_auto_product_faq_shortcode' );
اطلاعات محصول
TEXT - 2026-05-16 22:01:03
<?php /** * Auto Product FAQ for WooCommerce - WPCode Ready * نمایش خودکار سوالات پرتکرار محصول براساس ویژگی‌ها، تنوع‌ها و کلاس حمل‌ونقل */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * تنظیمات اصلی */ function mm_auto_faq_settings() { return array( // اگر true باشد، FAQ خودکار در صفحه محصول نمایش داده می‌شود 'auto_display' => true, // محل نمایش در صفحه محصول // woocommerce_after_single_product_summary = بعد از توضیحات و تب‌ها 'hook' => 'woocommerce_after_single_product_summary', 'priority' => 12, /** * نامک ویژگی‌ها * اگر ویژگی شما در ووکامرس با نام فارسی ساخته شده، ممکن است نامک آن مثلاً pa_rang یا pa_color باشد. * اینجا چند حالت رایج گذاشته شده. اگر نامک شما فرق دارد، اضافه‌اش کنید. */ 'color_attributes' => array( 'pa_color', 'pa_rang', 'pa_رنگ', 'color', 'rang', 'رنگ', ), 'size_attributes' => array( 'pa_size', 'pa_sizes', 'pa_sayz', 'pa_سایز', 'pa_dimensions', 'pa_abandaze', 'pa_ابعاد', 'size', 'سایز', 'dimensions', 'ابعاد', ), 'material_attributes' => array( 'pa_material', 'pa_jens', 'pa_جنس', 'material', 'jens', 'جنس', ), 'assembly_attributes' => array( 'pa_assembly', 'pa_montage', 'pa_مونتاژ', 'assembly', 'montage', 'مونتاژ', 'نیاز-به-نصب', 'pa_niyaz-be-nasb', ), /** * متن هزینه حمل بر اساس کلاس حمل‌ونقل * نکته مهم: * خود ووکامرس در کلاس حمل‌ونقل، مبلغ ثابت ذخیره نمی‌کند. * مبلغ حمل معمولاً داخل Shipping Zone و روش ارسال تعریف می‌شود. * بنابراین برای نمایش مبلغ دقیق، اینجا کلاس حمل را به متن یا مبلغ وصل می‌کنیم. * * کلیدها باید نامک Shipping Class باشند. * مثال: * 'heavy' => 'هزینه ارسال این محصول به‌صورت پس‌کرایه یا باربری محاسبه می‌شود.' */ 'shipping_class_messages' => array( // نمونه‌ها - مطابق کلاس‌های سایت خودت تغییر بده 'free-shipping' => 'ارسال این محصول رایگان است.', 'light' => 'هزینه ارسال این محصول سبک، طبق تعرفه پستی/تیپاکس محاسبه می‌شود.', 'medium' => 'هزینه ارسال این محصول طبق تعرفه حمل کالاهای متوسط محاسبه می‌شود.', 'heavy' => 'این محصول حجیم است و هزینه ارسال آن توسط باربری یا هماهنگی پشتیبانی اعلام می‌شود.', 'furniture' => 'هزینه حمل این محصول به دلیل ابعاد مبلمان، براساس شهر مقصد و باربری محاسبه می‌شود.', ), /** * اگر خواستی مبلغ ثابت برای کلاس حمل نمایش بدهی، اینجا فعال کن. * مبلغ‌ها به تومان/واحد پول فروشگاه طبق تنظیمات ووکامرس نمایش داده می‌شوند. */ 'shipping_class_prices' => array( // مثال: // 'light' => 150000, // 'medium' => 250000, // 'heavy' => 450000, // 'furniture' => 650000, ), ); } /** * گرفتن مقدار ویژگی محصول با چند نامک احتمالی */ function mm_get_product_attribute_values( $product, $possible_slugs = array() ) { if ( ! $product || empty( $possible_slugs ) ) { return array(); } $values = array(); foreach ( $possible_slugs as $slug ) { $slug = sanitize_title( $slug ); // حالت ویژگی عمومی ووکامرس مثل pa_color if ( taxonomy_exists( $slug ) ) { $terms = wc_get_product_terms( $product->get_id(), $slug, array( 'fields' => 'names' ) ); if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) { $values = array_merge( $values, $terms ); } } // حالت ویژگی سفارشی داخل خود محصول $attr_value = $product->get_attribute( $slug ); if ( ! empty( $attr_value ) ) { $parts = array_map( 'trim', explode( '|', $attr_value ) ); $values = array_merge( $values, $parts ); } } // اگر محصول متغیر است، از variation attributes هم بخوان if ( $product->is_type( 'variable' ) ) { $variation_attributes = $product->get_variation_attributes(); foreach ( $variation_attributes as $attr_key => $attr_options ) { $clean_key = str_replace( 'attribute_', '', $attr_key ); $clean_key = sanitize_title( $clean_key ); foreach ( $possible_slugs as $slug ) { $slug = sanitize_title( $slug ); if ( $clean_key === $slug || str_replace( 'pa_', '', $clean_key ) === str_replace( 'pa_', '', $slug ) ) { foreach ( $attr_options as $option ) { if ( taxonomy_exists( $clean_key ) ) { $term = get_term_by( 'slug', $option, $clean_key ); if ( $term && ! is_wp_error( $term ) ) { $values[] = $term->name; } else { $values[] = $option; } } else { $values[] = $option; } } } } } } $values = array_filter( array_map( 'trim', $values ) ); $values = array_unique( $values ); return $values; } /** * ساخت متن خوانا از آرایه */ function mm_human_list( $items ) { $items = array_filter( array_unique( array_map( 'trim', (array) $items ) ) ); if ( empty( $items ) ) { return ''; } return implode( '، ', $items ); } /** * گرفتن متن حمل‌ونقل */ function mm_get_shipping_faq_text( $product ) { $settings = mm_auto_faq_settings(); if ( ! $product ) { return ''; } $shipping_class_id = $product->get_shipping_class_id(); $shipping_class = $product->get_shipping_class(); if ( ! $shipping_class_id || empty( $shipping_class ) ) { return 'هزینه ارسال این محصول در مرحله ثبت سفارش و براساس آدرس و روش ارسال محاسبه می‌شود.'; } $term = get_term( $shipping_class_id, 'product_shipping_class' ); $class_name = ''; $class_slug = $shipping_class; if ( $term && ! is_wp_error( $term ) ) { $class_name = $term->name; $class_slug = $term->slug; } // اگر برای کلاس حمل مبلغ ثابت تعریف شده باشد if ( isset( $settings['shipping_class_prices'][ $class_slug ] ) && $settings['shipping_class_prices'][ $class_slug ] !== '' ) { $price = floatval( $settings['shipping_class_prices'][ $class_slug ] ); return 'کلاس حمل‌ونقل این محصول «' . esc_html( $class_name ) . '» است و هزینه ارسال آن حدوداً ' . wp_kses_post( wc_price( $price ) ) . ' می‌باشد.'; } // اگر برای کلاس حمل پیام اختصاصی تعریف شده باشد if ( isset( $settings['shipping_class_messages'][ $class_slug ] ) && ! empty( $settings['shipping_class_messages'][ $class_slug ] ) ) { return esc_html( $settings['shipping_class_messages'][ $class_slug ] ); } if ( ! empty( $class_name ) ) { return 'کلاس حمل‌ونقل این محصول «' . esc_html( $class_name ) . '» است و هزینه ارسال براساس شهر مقصد و روش ارسال محاسبه می‌شود.'; } return 'هزینه ارسال این محصول در مرحله ثبت سفارش محاسبه می‌شود.'; } /** * تولید FAQ محصول */ function mm_generate_auto_product_faq_html( $product_id = 0 ) { if ( ! function_exists( 'wc_get_product' ) ) { return ''; } if ( ! $product_id ) { global $product; if ( $product && is_a( $product, 'WC_Product' ) ) { $product_id = $product->get_id(); } } $product = wc_get_product( $product_id ); if ( ! $product ) { return ''; } $settings = mm_auto_faq_settings(); $faq_items = array(); /** * رنگ‌ها */ $colors = mm_get_product_attribute_values( $product, $settings['color_attributes'] ); if ( ! empty( $colors ) ) { $faq_items[] = array( 'q' => 'این محصول در چه رنگ‌هایی قابل سفارش است؟', 'a' => 'این محصول در رنگ‌های ' . esc_html( mm_human_list( $colors ) ) . ' قابل سفارش است.', ); } /** * سایز / ابعاد از ویژگی‌ها */ $sizes = mm_get_product_attribute_values( $product, $settings['size_attributes'] ); if ( ! empty( $sizes ) ) { $faq_items[] = array( 'q' => 'سایز یا ابعاد این محصول چقدر است؟', 'a' => 'سایز/ابعاد ثبت‌شده برای این محصول: ' . esc_html( mm_human_list( $sizes ) ) . '.', ); } /** * ابعاد استاندارد ووکامرس */ if ( $product->has_dimensions() ) { $faq_items[] = array( 'q' => 'ابعاد بسته یا محصول چقدر است؟', 'a' => 'ابعاد ثبت‌شده محصول: ' . esc_html( wc_format_dimensions( $product->get_dimensions( false ) ) ) . '.', ); } /** * جنس */ $materials = mm_get_product_attribute_values( $product, $settings['material_attributes'] ); if ( ! empty( $materials ) ) { $faq_items[] = array( 'q' => 'جنس این محصول چیست؟', 'a' => 'جنس این محصول: ' . esc_html( mm_human_list( $materials ) ) . '.', ); } /** * وزن */ if ( $product->has_weight() ) { $faq_items[] = array( 'q' => 'وزن این محصول چقدر است؟', 'a' => 'وزن ثبت‌شده برای این محصول ' . esc_html( wc_format_weight( $product->get_weight() ) ) . ' است.', ); } /** * حمل و نقل */ $shipping_text = mm_get_shipping_faq_text( $product ); if ( ! empty( $shipping_text ) ) { $faq_items[] = array( 'q' => 'هزینه ارسال این محصول چقدر است؟', 'a' => $shipping_text, ); } /** * موجودی */ if ( $product->managing_stock() ) { $stock_quantity = $product->get_stock_quantity(); if ( $product->is_in_stock() && $stock_quantity !== null ) { $faq_items[] = array( 'q' => 'آیا این محصول موجود است؟', 'a' => 'بله، این محصول موجود است. تعداد موجودی فعلی: ' . esc_html( $stock_quantity ) . ' عدد.', ); } elseif ( $product->is_in_stock() ) { $faq_items[] = array( 'q' => 'آیا این محصول موجود است؟', 'a' => 'بله، این محصول در حال حاضر موجود است.', ); } else { $faq_items[] = array( 'q' => 'آیا این محصول موجود است؟', 'a' => 'این محصول در حال حاضر ناموجود است.', ); } } else { if ( $product->is_in_stock() ) { $faq_items[] = array( 'q' => 'آیا این محصول موجود است؟', 'a' => 'بله، این محصول در حال حاضر موجود است.', ); } } /** * مونتاژ / نصب */ $assembly = mm_get_product_attribute_values( $product, $settings['assembly_attributes'] ); if ( ! empty( $assembly ) ) { $faq_items[] = array( 'q' => 'آیا این محصول نیاز به نصب یا مونتاژ دارد؟', 'a' => esc_html( mm_human_list( $assembly ) ), ); } /** * اگر هیچ آیتمی نبود، چیزی نمایش نده */ if ( empty( $faq_items ) ) { return ''; } ob_start(); ?> <section class="mm-auto-product-faq" dir="rtl"> <h2 class="mm-auto-product-faq-title">سوالات پرتکرار این محصول</h2> <div class="mm-auto-product-faq-list"> <?php foreach ( $faq_items as $index => $item ) : ?> <details class="mm-auto-product-faq-item" <?php echo $index === 0 ? 'open' : ''; ?>> <summary class="mm-auto-product-faq-question"> <?php echo esc_html( $item['q'] ); ?> </summary> <div class="mm-auto-product-faq-answer"> <?php echo wp_kses_post( wpautop( $item['a'] ) ); ?> </div> </details> <?php endforeach; ?> </div> </section> <style> .mm-auto-product-faq { margin: 35px 0; padding: 22px; background: #fff; border: 1px solid #e8e8e8; border-radius: 14px; direction: rtl; text-align: right; } .mm-auto-product-faq-title { margin: 0 0 18px; font-size: 22px; font-weight: 700; line-height: 1.6; } .mm-auto-product-faq-list { display: grid; gap: 10px; } .mm-auto-product-faq-item { border: 1px solid #eeeeee; border-radius: 10px; background: #fafafa; overflow: hidden; } .mm-auto-product-faq-question { cursor: pointer; padding: 14px 16px; font-weight: 700; color: #222; list-style: none; } .mm-auto-product-faq-question::-webkit-details-marker { display: none; } .mm-auto-product-faq-question:after { content: "+"; float: left; font-size: 20px; line-height: 1; } .mm-auto-product-faq-item[open] .mm-auto-product-faq-question:after { content: "-"; } .mm-auto-product-faq-answer { padding: 0 16px 14px; color: #555; line-height: 2; font-size: 15px; } .mm-auto-product-faq-answer p { margin: 0; } </style> <?php return ob_get_clean(); } /** * نمایش خودکار در صفحه محصول */ function mm_display_auto_product_faq() { if ( ! is_product() ) { return; } echo mm_generate_auto_product_faq_html(); } $mm_settings = mm_auto_faq_settings(); if ( ! empty( $mm_settings['auto_display'] ) ) { add_action( $mm_settings['hook'], 'mm_display_auto_product_faq', intval( $mm_settings['priority'] ) ); } /** * شورت‌کد اختیاری * استفاده: * [auto_product_faq] */ function mm_auto_product_faq_shortcode( $atts ) { $atts = shortcode_atts( array( 'id' => 0, ), $atts, 'auto_product_faq' ); return mm_generate_auto_product_faq_html( intval( $atts['id'] ) ); } add_shortcode( 'auto_product_faq', 'mm_auto_product_faq_shortcode' );
<?php
/**
 * Auto Product FAQ for WooCommerce - WPCode Ready
 * نمایش خودکار سوالات پرتکرار محصول براساس ویژگی‌ها، تنوع‌ها و کلاس حمل‌ونقل
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * تنظیمات اصلی
 */
function mm_auto_faq_settings() {
    return array(
        // اگر true باشد، FAQ خودکار در صفحه محصول نمایش داده می‌شود
        'auto_display' => true,

        // محل نمایش در صفحه محصول
        // woocommerce_after_single_product_summary = بعد از توضیحات و تب‌ها
        'hook'     => 'woocommerce_after_single_product_summary',
        'priority' => 12,

        /**
         * نامک ویژگی‌ها
         * اگر ویژگی شما در ووکامرس با نام فارسی ساخته شده، ممکن است نامک آن مثلاً pa_rang یا pa_color باشد.
         * اینجا چند حالت رایج گذاشته شده. اگر نامک شما فرق دارد، اضافه‌اش کنید.
         */
        'color_attributes' => array(
            'pa_color',
            'pa_rang',
            'pa_رنگ',
            'color',
            'rang',
            'رنگ',
        ),

        'size_attributes' => array(
            'pa_size',
            'pa_sizes',
            'pa_sayz',
            'pa_سایز',
            'pa_dimensions',
            'pa_abandaze',
            'pa_ابعاد',
            'size',
            'سایز',
            'dimensions',
            'ابعاد',
        ),

        'material_attributes' => array(
            'pa_material',
            'pa_jens',
            'pa_جنس',
            'material',
            'jens',
            'جنس',
        ),

        'assembly_attributes' => array(
            'pa_assembly',
            'pa_montage',
            'pa_مونتاژ',
            'assembly',
            'montage',
            'مونتاژ',
            'نیاز-به-نصب',
            'pa_niyaz-be-nasb',
        ),

        /**
         * متن هزینه حمل بر اساس کلاس حمل‌ونقل
         * نکته مهم:
         * خود ووکامرس در کلاس حمل‌ونقل، مبلغ ثابت ذخیره نمی‌کند.
         * مبلغ حمل معمولاً داخل Shipping Zone و روش ارسال تعریف می‌شود.
         * بنابراین برای نمایش مبلغ دقیق، اینجا کلاس حمل را به متن یا مبلغ وصل می‌کنیم.
         *
         * کلیدها باید نامک Shipping Class باشند.
         * مثال:
         * 'heavy' => 'هزینه ارسال این محصول به‌صورت پس‌کرایه یا باربری محاسبه می‌شود.'
         */
        'shipping_class_messages' => array(
            // نمونه‌ها - مطابق کلاس‌های سایت خودت تغییر بده
            'free-shipping' => 'ارسال این محصول رایگان است.',
            'light'         => 'هزینه ارسال این محصول سبک، طبق تعرفه پستی/تیپاکس محاسبه می‌شود.',
            'medium'        => 'هزینه ارسال این محصول طبق تعرفه حمل کالاهای متوسط محاسبه می‌شود.',
            'heavy'         => 'این محصول حجیم است و هزینه ارسال آن توسط باربری یا هماهنگی پشتیبانی اعلام می‌شود.',
            'furniture'     => 'هزینه حمل این محصول به دلیل ابعاد مبلمان، براساس شهر مقصد و باربری محاسبه می‌شود.',
        ),

        /**
         * اگر خواستی مبلغ ثابت برای کلاس حمل نمایش بدهی، اینجا فعال کن.
         * مبلغ‌ها به تومان/واحد پول فروشگاه طبق تنظیمات ووکامرس نمایش داده می‌شوند.
         */
        'shipping_class_prices' => array(
            // مثال:
            // 'light'     => 150000,
            // 'medium'    => 250000,
            // 'heavy'     => 450000,
            // 'furniture' => 650000,
        ),
    );
}

/**
 * گرفتن مقدار ویژگی محصول با چند نامک احتمالی
 */
function mm_get_product_attribute_values( $product, $possible_slugs = array() ) {
    if ( ! $product || empty( $possible_slugs ) ) {
        return array();
    }

    $values = array();

    foreach ( $possible_slugs as $slug ) {
        $slug = sanitize_title( $slug );

        // حالت ویژگی عمومی ووکامرس مثل pa_color
        if ( taxonomy_exists( $slug ) ) {
            $terms = wc_get_product_terms( $product->get_id(), $slug, array( 'fields' => 'names' ) );
            if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
                $values = array_merge( $values, $terms );
            }
        }

        // حالت ویژگی سفارشی داخل خود محصول
        $attr_value = $product->get_attribute( $slug );
        if ( ! empty( $attr_value ) ) {
            $parts = array_map( 'trim', explode( '|', $attr_value ) );
            $values = array_merge( $values, $parts );
        }
    }

    // اگر محصول متغیر است، از variation attributes هم بخوان
    if ( $product->is_type( 'variable' ) ) {
        $variation_attributes = $product->get_variation_attributes();

        foreach ( $variation_attributes as $attr_key => $attr_options ) {
            $clean_key = str_replace( 'attribute_', '', $attr_key );
            $clean_key = sanitize_title( $clean_key );

            foreach ( $possible_slugs as $slug ) {
                $slug = sanitize_title( $slug );

                if ( $clean_key === $slug || str_replace( 'pa_', '', $clean_key ) === str_replace( 'pa_', '', $slug ) ) {
                    foreach ( $attr_options as $option ) {
                        if ( taxonomy_exists( $clean_key ) ) {
                            $term = get_term_by( 'slug', $option, $clean_key );
                            if ( $term && ! is_wp_error( $term ) ) {
                                $values[] = $term->name;
                            } else {
                                $values[] = $option;
                            }
                        } else {
                            $values[] = $option;
                        }
                    }
                }
            }
        }
    }

    $values = array_filter( array_map( 'trim', $values ) );
    $values = array_unique( $values );

    return $values;
}

/**
 * ساخت متن خوانا از آرایه
 */
function mm_human_list( $items ) {
    $items = array_filter( array_unique( array_map( 'trim', (array) $items ) ) );

    if ( empty( $items ) ) {
        return '';
    }

    return implode( '، ', $items );
}

/**
 * گرفتن متن حمل‌ونقل
 */
function mm_get_shipping_faq_text( $product ) {
    $settings = mm_auto_faq_settings();

    if ( ! $product ) {
        return '';
    }

    $shipping_class_id = $product->get_shipping_class_id();
    $shipping_class    = $product->get_shipping_class();

    if ( ! $shipping_class_id || empty( $shipping_class ) ) {
        return 'هزینه ارسال این محصول در مرحله ثبت سفارش و براساس آدرس و روش ارسال محاسبه می‌شود.';
    }

    $term = get_term( $shipping_class_id, 'product_shipping_class' );

    $class_name = '';
    $class_slug = $shipping_class;

    if ( $term && ! is_wp_error( $term ) ) {
        $class_name = $term->name;
        $class_slug = $term->slug;
    }

    // اگر برای کلاس حمل مبلغ ثابت تعریف شده باشد
    if ( isset( $settings['shipping_class_prices'][ $class_slug ] ) && $settings['shipping_class_prices'][ $class_slug ] !== '' ) {
        $price = floatval( $settings['shipping_class_prices'][ $class_slug ] );

        return 'کلاس حمل‌ونقل این محصول «' . esc_html( $class_name ) . '» است و هزینه ارسال آن حدوداً ' . wp_kses_post( wc_price( $price ) ) . ' می‌باشد.';
    }

    // اگر برای کلاس حمل پیام اختصاصی تعریف شده باشد
    if ( isset( $settings['shipping_class_messages'][ $class_slug ] ) && ! empty( $settings['shipping_class_messages'][ $class_slug ] ) ) {
        return esc_html( $settings['shipping_class_messages'][ $class_slug ] );
    }

    if ( ! empty( $class_name ) ) {
        return 'کلاس حمل‌ونقل این محصول «' . esc_html( $class_name ) . '» است و هزینه ارسال براساس شهر مقصد و روش ارسال محاسبه می‌شود.';
    }

    return 'هزینه ارسال این محصول در مرحله ثبت سفارش محاسبه می‌شود.';
}

/**
 * تولید FAQ محصول
 */
function mm_generate_auto_product_faq_html( $product_id = 0 ) {
    if ( ! function_exists( 'wc_get_product' ) ) {
        return '';
    }

    if ( ! $product_id ) {
        global $product;
        if ( $product && is_a( $product, 'WC_Product' ) ) {
            $product_id = $product->get_id();
        }
    }

    $product = wc_get_product( $product_id );

    if ( ! $product ) {
        return '';
    }

    $settings = mm_auto_faq_settings();

    $faq_items = array();

    /**
     * رنگ‌ها
     */
    $colors = mm_get_product_attribute_values( $product, $settings['color_attributes'] );
    if ( ! empty( $colors ) ) {
        $faq_items[] = array(
            'q' => 'این محصول در چه رنگ‌هایی قابل سفارش است؟',
            'a' => 'این محصول در رنگ‌های ' . esc_html( mm_human_list( $colors ) ) . ' قابل سفارش است.',
        );
    }

    /**
     * سایز / ابعاد از ویژگی‌ها
     */
    $sizes = mm_get_product_attribute_values( $product, $settings['size_attributes'] );
    if ( ! empty( $sizes ) ) {
        $faq_items[] = array(
            'q' => 'سایز یا ابعاد این محصول چقدر است؟',
            'a' => 'سایز/ابعاد ثبت‌شده برای این محصول: ' . esc_html( mm_human_list( $sizes ) ) . '.',
        );
    }

    /**
     * ابعاد استاندارد ووکامرس
     */
    if ( $product->has_dimensions() ) {
        $faq_items[] = array(
            'q' => 'ابعاد بسته یا محصول چقدر است؟',
            'a' => 'ابعاد ثبت‌شده محصول: ' . esc_html( wc_format_dimensions( $product->get_dimensions( false ) ) ) . '.',
        );
    }

    /**
     * جنس
     */
    $materials = mm_get_product_attribute_values( $product, $settings['material_attributes'] );
    if ( ! empty( $materials ) ) {
        $faq_items[] = array(
            'q' => 'جنس این محصول چیست؟',
            'a' => 'جنس این محصول: ' . esc_html( mm_human_list( $materials ) ) . '.',
        );
    }

    /**
     * وزن
     */
    if ( $product->has_weight() ) {
        $faq_items[] = array(
            'q' => 'وزن این محصول چقدر است؟',
            'a' => 'وزن ثبت‌شده برای این محصول ' . esc_html( wc_format_weight( $product->get_weight() ) ) . ' است.',
        );
    }

    /**
     * حمل و نقل
     */
    $shipping_text = mm_get_shipping_faq_text( $product );
    if ( ! empty( $shipping_text ) ) {
        $faq_items[] = array(
            'q' => 'هزینه ارسال این محصول چقدر است؟',
            'a' => $shipping_text,
        );
    }

    /**
     * موجودی
     */
    if ( $product->managing_stock() ) {
        $stock_quantity = $product->get_stock_quantity();

        if ( $product->is_in_stock() && $stock_quantity !== null ) {
            $faq_items[] = array(
                'q' => 'آیا این محصول موجود است؟',
                'a' => 'بله، این محصول موجود است. تعداد موجودی فعلی: ' . esc_html( $stock_quantity ) . ' عدد.',
            );
        } elseif ( $product->is_in_stock() ) {
            $faq_items[] = array(
                'q' => 'آیا این محصول موجود است؟',
                'a' => 'بله، این محصول در حال حاضر موجود است.',
            );
        } else {
            $faq_items[] = array(
                'q' => 'آیا این محصول موجود است؟',
                'a' => 'این محصول در حال حاضر ناموجود است.',
            );
        }
    } else {
        if ( $product->is_in_stock() ) {
            $faq_items[] = array(
                'q' => 'آیا این محصول موجود است؟',
                'a' => 'بله، این محصول در حال حاضر موجود است.',
            );
        }
    }

    /**
     * مونتاژ / نصب
     */
    $assembly = mm_get_product_attribute_values( $product, $settings['assembly_attributes'] );
    if ( ! empty( $assembly ) ) {
        $faq_items[] = array(
            'q' => 'آیا این محصول نیاز به نصب یا مونتاژ دارد؟',
            'a' => esc_html( mm_human_list( $assembly ) ),
        );
    }

    /**
     * اگر هیچ آیتمی نبود، چیزی نمایش نده
     */
    if ( empty( $faq_items ) ) {
        return '';
    }

    ob_start();
    ?>

    <section class="mm-auto-product-faq" dir="rtl">
        <h2 class="mm-auto-product-faq-title">سوالات پرتکرار این محصول</h2>

        <div class="mm-auto-product-faq-list">
            <?php foreach ( $faq_items as $index => $item ) : ?>
                <details class="mm-auto-product-faq-item" <?php echo $index === 0 ? 'open' : ''; ?>>
                    <summary class="mm-auto-product-faq-question">
                        <?php echo esc_html( $item['q'] ); ?>
                    </summary>
                    <div class="mm-auto-product-faq-answer">
                        <?php echo wp_kses_post( wpautop( $item['a'] ) ); ?>
                    </div>
                </details>
            <?php endforeach; ?>
        </div>
    </section>

    <style>
        .mm-auto-product-faq {
            margin: 35px 0;
            padding: 22px;
            background: #fff;
            border: 1px solid #e8e8e8;
            border-radius: 14px;
            direction: rtl;
            text-align: right;
        }

        .mm-auto-product-faq-title {
            margin: 0 0 18px;
            font-size: 22px;
            font-weight: 700;
            line-height: 1.6;
        }

        .mm-auto-product-faq-list {
            display: grid;
            gap: 10px;
        }

        .mm-auto-product-faq-item {
            border: 1px solid #eeeeee;
            border-radius: 10px;
            background: #fafafa;
            overflow: hidden;
        }

        .mm-auto-product-faq-question {
            cursor: pointer;
            padding: 14px 16px;
            font-weight: 700;
            color: #222;
            list-style: none;
        }

        .mm-auto-product-faq-question::-webkit-details-marker {
            display: none;
        }

        .mm-auto-product-faq-question:after {
            content: "+";
            float: left;
            font-size: 20px;
            line-height: 1;
        }

        .mm-auto-product-faq-item[open] .mm-auto-product-faq-question:after {
            content: "-";
        }

        .mm-auto-product-faq-answer {
            padding: 0 16px 14px;
            color: #555;
            line-height: 2;
            font-size: 15px;
        }

        .mm-auto-product-faq-answer p {
            margin: 0;
        }
    </style>

    <?php
    return ob_get_clean();
}

/**
 * نمایش خودکار در صفحه محصول
 */
function mm_display_auto_product_faq() {
    if ( ! is_product() ) {
        return;
    }

    echo mm_generate_auto_product_faq_html();
}

$mm_settings = mm_auto_faq_settings();

if ( ! empty( $mm_settings['auto_display'] ) ) {
    add_action(
        $mm_settings['hook'],
        'mm_display_auto_product_faq',
        intval( $mm_settings['priority'] )
    );
}

/**
 * شورت‌کد اختیاری
 * استفاده:
 * [auto_product_faq]
 */
function mm_auto_product_faq_shortcode( $atts ) {
    $atts = shortcode_atts(
        array(
            'id' => 0,
        ),
        $atts,
        'auto_product_faq'
    );

    return mm_generate_auto_product_faq_html( intval( $atts['id'] ) );
}
add_shortcode( 'auto_product_faq', 'mm_auto_product_faq_shortcode' );
کد عالی اپ
TEXT - 2026-05-12 01:13:18
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage"></small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } * { box-sizing: border-box; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-wrap .summary-card { background: #ffffff; color: #111827; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-wrap .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-wrap .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: none; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } @media (max-width: 380px) { .summary-wrap .summary-card strong { font-size: 14px; } } </style> <script> (function(){ let selectedService = null; let records = []; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const toast = document.getElementById("toast"); const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); } function updatePersonalRecord() { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = ""; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); showToast("ثبت جدید اضافه شد"); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmountValue); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderRecords(); updateSummary(); updatePersonalRecord(); renderFeedback(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="worker-page">

          <div class="page-header">
            <h1 class="page-title">ثبت کار امروز</h1>
            <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
          </div>

          <div class="search-card">
            <label class="search-label">جستجوی خدمت</label>

            <div class="search-input-wrap">
              <div class="search-icon">🔍</div>
              <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
            </div>

            <div class="service-results" id="serviceResults"></div>
          </div>

          <div class="summary-wrap">
            <div class="summary-card">
              <span>مبلغ امروز</span>
              <strong id="todayAmount">۰ تومان</strong>
            </div>

            <div class="summary-card">
              <span>تعداد امروز</span>
              <strong id="todayCount">۰</strong>
            </div>
          </div>

          <div class="personal-record-card">
            <div class="record-icon">🏆</div>
            <div class="record-content">
              <span>رکورد روزانه تو</span>
              <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
              <small id="recordMessage"></small>
            </div>
          </div>

          <div class="feedback-card">
            <div class="feedback-title">آخرین بازخورد عملکرد</div>
            <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
            <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
            <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
          </div>

          <div class="form-card" id="serviceForm">
            <div class="selected-service">
              <span>خدمت انتخاب شده</span>
              <strong id="selectedServiceName">---</strong>
            </div>

            <div class="form-grid">
              <div class="field">
                <label>تعداد</label>
                <input type="number" id="serviceCount" min="1" value="1" />
              </div>

              <div class="field">
                <label>مقدار / مبلغ واحد</label>
                <input type="number" id="servicePrice" min="0" />
              </div>
            </div>

            <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

            <div class="details-box" id="detailsBox">
              <div class="field">
                <label>توضیحات</label>
                <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
              </div>
            </div>

            <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
          </div>

          <div class="records-card">
            <div class="records-title">
              <strong>ثبت‌های امروز</strong>
              <span id="recordsCountText">۰ مورد</span>
            </div>

            <div id="recordsList">
              <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
            </div>
          </div>

        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  * {
    box-sizing: border-box;
  }

  .worker-page {
    max-width: 520px;
    margin: 0 auto;
  }

  .page-header {
    margin-bottom: 14px;
  }

  .page-title {
    font-size: 18px;
    font-weight: 900;
    margin: 0 0 5px;
    color: #111827;
  }

  .page-subtitle {
    font-size: 12px;
    color: #6b7280;
    margin: 0;
    line-height: 1.8;
  }

  .search-card,
  .form-card,
  .records-card {
    background: #ffffff;
    border-radius: 20px;
    padding: 13px;
    box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
    margin-bottom: 13px;
    border: 1px solid #e5e7eb;
  }

  .search-label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    margin-bottom: 8px;
    color: #374151;
  }

  .search-input-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
    background: #f9fafb;
    border: 2px solid #2563eb;
    border-radius: 15px;
    padding: 10px 12px;
  }

  .search-icon {
    font-size: 17px;
  }

  #serviceSearch {
    width: 100%;
    border: none;
    outline: none;
    background: transparent;
    font-size: 14px;
    font-weight: 700;
    color: #111827;
  }

  #serviceSearch::placeholder {
    color: #9ca3af;
    font-weight: 500;
  }

  .service-results {
    margin-top: 10px;
    display: none;
  }

  .service-result-item {
    background: #f8fafc;
    border: 1px solid #e5e7eb;
    border-radius: 13px;
    padding: 10px;
    margin-bottom: 7px;
    cursor: pointer;
  }

  .service-result-item:hover {
    background: #eef2ff;
    border-color: #c7d2fe;
  }

  .service-result-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 3px;
  }

  .service-result-price {
    font-size: 11px;
    color: #6b7280;
  }

  .summary-wrap {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 12px;
  }

  .summary-wrap .summary-card {
    background: #ffffff;
    color: #111827;
    border-radius: 17px;
    padding: 12px;
    box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
    border: 1px solid #e5e7eb;
  }

  .summary-wrap .summary-card span {
    display: block;
    color: #6b7280;
    font-size: 11px;
    font-weight: 700;
    margin-bottom: 6px;
  }

  .summary-wrap .summary-card strong {
    display: block;
    color: #111827;
    font-size: 15px;
    font-weight: 900;
  }

  #todayAmount {
    color: #16a34a;
  }

  .personal-record-card {
    background: linear-gradient(135deg, #fff7ed, #fffbeb);
    border: 1px solid #fed7aa;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    display: flex;
    align-items: center;
    gap: 11px;
    box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
  }

  .record-icon {
    width: 42px;
    height: 42px;
    border-radius: 14px;
    background: #ffedd5;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 21px;
    flex-shrink: 0;
  }

  .record-content {
    flex: 1;
  }

  .record-content span {
    display: block;
    color: #9a3412;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content strong {
    display: block;
    color: #111827;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content small {
    display: none;
    color: #92400e;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.7;
  }

  .feedback-card {
    background: linear-gradient(135deg, #eff6ff, #f8fafc);
    border: 1px solid #bfdbfe;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
  }

  .feedback-title {
    font-size: 12px;
    font-weight: 900;
    color: #1d4ed8;
    margin-bottom: 7px;
  }

  .feedback-main {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 5px;
  }

  .feedback-sub {
    font-size: 11px;
    line-height: 1.8;
    color: #4b5563;
  }

  .feedback-badge {
    display: inline-block;
    margin-top: 8px;
    padding: 5px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 900;
  }

  .feedback-badge.positive {
    background: #dbeafe;
    color: #1d4ed8;
  }

  .feedback-badge.negative {
    background: #fef3c7;
    color: #92400e;
  }

  .feedback-badge.neutral {
    background: #e5e7eb;
    color: #374151;
  }

  .form-card {
    display: none;
  }

  .selected-service {
    background: #eff6ff;
    border: 1px solid #bfdbfe;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 12px;
  }

  .selected-service span {
    display: block;
    color: #1d4ed8;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .selected-service strong {
    display: block;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .field {
    margin-bottom: 10px;
  }

  .field label {
    display: block;
    font-size: 11px;
    font-weight: 900;
    color: #374151;
    margin-bottom: 6px;
  }

  .field input,
  .field textarea {
    width: 100%;
    border: 1px solid #d1d5db;
    outline: none;
    background: #f9fafb;
    border-radius: 13px;
    padding: 10px;
    font-size: 13px;
    font-family: inherit;
  }

  .field input:focus,
  .field textarea:focus {
    border-color: #2563eb;
    background: #ffffff;
  }

  .field textarea {
    min-height: 75px;
    resize: vertical;
    line-height: 1.8;
  }

  .details-toggle {
    width: 100%;
    border: none;
    background: #f3f4f6;
    color: #374151;
    border-radius: 13px;
    padding: 10px;
    font-size: 12px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
    margin-bottom: 10px;
  }

  .details-box {
    display: none;
  }

  .submit-btn {
    width: 100%;
    border: none;
    background: #2563eb;
    color: #ffffff;
    border-radius: 15px;
    padding: 12px;
    font-size: 14px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
  }

  .records-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }

  .records-title strong {
    font-size: 14px;
    font-weight: 900;
    color: #111827;
  }

  .records-title span {
    font-size: 11px;
    color: #6b7280;
    font-weight: 700;
  }

  .empty-records {
    background: #f9fafb;
    color: #6b7280;
    text-align: center;
    border-radius: 14px;
    padding: 16px 10px;
    font-size: 12px;
    line-height: 1.8;
  }

  .record-item {
    border: 1px solid #e5e7eb;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 9px;
    background: #ffffff;
  }

  .record-item:last-child {
    margin-bottom: 0;
  }

  .record-top {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 7px;
  }

  .record-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
  }

  .record-time {
    font-size: 10px;
    color: #9ca3af;
    white-space: nowrap;
  }

  .record-info {
    font-size: 11px;
    color: #4b5563;
    line-height: 1.9;
  }

  .record-total {
    margin-top: 6px;
    font-size: 12px;
    font-weight: 900;
    color: #16a34a;
  }

  .record-desc {
    margin-top: 5px;
    color: #6b7280;
    font-size: 11px;
    line-height: 1.8;
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }

  @media (max-width: 380px) {
    .summary-wrap .summary-card strong {
      font-size: 14px;
    }
  }
</style>

<script>
(function(){
  let selectedService = null;
  let records = [];

  let latestFeedback = {
    type: "positive",
    title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
    description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
    badge: "۸۰٪"
  };

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const services = [
    { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
    { name: "جوشکاری", price: 200000 },
    { name: "نجاری", price: 180000 }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const toast = document.getElementById("toast");

  const serviceSearch = document.getElementById("serviceSearch");
  const serviceResults = document.getElementById("serviceResults");
  const serviceForm = document.getElementById("serviceForm");
  const selectedServiceName = document.getElementById("selectedServiceName");
  const serviceCount = document.getElementById("serviceCount");
  const servicePrice = document.getElementById("servicePrice");
  const serviceDescription = document.getElementById("serviceDescription");
  const submitService = document.getElementById("submitService");
  const todayAmount = document.getElementById("todayAmount");
  const todayCount = document.getElementById("todayCount");
  const recordsList = document.getElementById("recordsList");
  const recordsCountText = document.getElementById("recordsCountText");
  const detailsToggle = document.getElementById("detailsToggle");
  const detailsBox = document.getElementById("detailsBox");
  const bestRecordText = document.getElementById("bestRecordText");
  const recordMessage = document.getElementById("recordMessage");
  const feedbackMain = document.getElementById("feedbackMain");
  const feedbackSub = document.getElementById("feedbackSub");
  const feedbackBadge = document.getElementById("feedbackBadge");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function toPersianNumber(value) {
    return Number(value || 0).toLocaleString("fa-IR");
  }

  function formatToman(value) {
    return toPersianNumber(value) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function showResults(keyword) {
    const text = keyword.trim();
    serviceResults.innerHTML = "";

    if (!text) {
      serviceResults.style.display = "none";
      return;
    }

    const filtered = services.filter(function(service) {
      return service.name.includes(text);
    });

    if (filtered.length === 0) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
        <div class="service-result-price">برای انتخاب این مورد بزنید</div>
      `;
      item.addEventListener("click", function() {
        selectService({ name: text, price: 0 });
      });
      serviceResults.appendChild(item);
      serviceResults.style.display = "block";
      return;
    }

    filtered.forEach(function(service) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">${service.name}</div>
        <div class="service-result-price">${formatToman(service.price)}</div>
      `;
      item.addEventListener("click", function() {
        selectService(service);
      });
      serviceResults.appendChild(item);
    });

    serviceResults.style.display = "block";
  }

  function selectService(service) {
    selectedService = service;
    selectedServiceName.textContent = service.name;
    serviceSearch.value = service.name;
    servicePrice.value = service.price || "";
    serviceCount.value = 1;
    serviceDescription.value = "";
    serviceResults.style.display = "none";
    serviceForm.style.display = "block";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";

    setTimeout(function() {
      serviceCount.focus();
    }, 100);
  }

  function updateSummary() {
    const totalAmount = records.reduce(function(sum, item) {
      return sum + item.total;
    }, 0);

    const totalCount = records.reduce(function(sum, item) {
      return sum + item.count;
    }, 0);

    todayAmount.textContent = formatToman(totalAmount);
    todayCount.textContent = toPersianNumber(totalCount);
  }

  function updatePersonalRecord() {
    bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
    recordMessage.textContent = "";
  }

  function renderRecords() {
    recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

    if (records.length === 0) {
      recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
      return;
    }

    recordsList.innerHTML = "";

    const reversed = records.slice().reverse();
    reversed.forEach(function(item) {
      const div = document.createElement("div");
      div.className = "record-item";
      div.innerHTML = `
        <div class="record-top">
          <div class="record-name">${item.name}</div>
          <div class="record-time">${item.time}</div>
        </div>
        <div class="record-info">
          تعداد: ${toPersianNumber(item.count)} |
          مبلغ واحد: ${formatToman(item.price)}
        </div>
        <div class="record-total">
          جمع: ${formatToman(item.total)}
        </div>
        ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
      `;
      recordsList.appendChild(div);
    });
  }

  function renderFeedback() {
    feedbackMain.textContent = latestFeedback.title;
    feedbackSub.textContent = latestFeedback.description;
    feedbackBadge.textContent = latestFeedback.badge;
    feedbackBadge.className = "feedback-badge " + latestFeedback.type;
  }

  function submitRecord() {
    if (!selectedService) {
      alert("اول یک خدمت را انتخاب کن.");
      return;
    }

    const count = parseInt(serviceCount.value, 10);
    const price = parseInt(servicePrice.value, 10);
    const description = serviceDescription.value.trim();

    if (!count || count <= 0) {
      alert("تعداد را درست وارد کن.");
      return;
    }

    if (isNaN(price) || price < 0) {
      alert("مبلغ را درست وارد کن.");
      return;
    }

    const total = count * price;
    const now = new Date();

    records.push({
      name: selectedService.name,
      count: count,
      price: price,
      total: total,
      description: description,
      time: now.toLocaleTimeString("fa-IR", {
        hour: "2-digit",
        minute: "2-digit"
      })
    });

    renderRecords();
    updateSummary();

    selectedService = null;
    serviceSearch.value = "";
    serviceCount.value = 1;
    servicePrice.value = "";
    serviceDescription.value = "";
    selectedServiceName.textContent = "---";
    serviceForm.style.display = "none";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
    serviceSearch.focus();

    showToast("ثبت جدید اضافه شد");
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmountValue);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderRecords();
    updateSummary();
    updatePersonalRecord();
    renderFeedback();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function() {
    showResults(serviceSearch.value);
  });

  detailsToggle.addEventListener("click", function() {
    if (detailsBox.style.display === "block") {
      detailsBox.style.display = "none";
      detailsToggle.textContent = "افزودن توضیحات اختیاری";
    } else {
      detailsBox.style.display = "block";
      detailsToggle.textContent = "بستن توضیحات";
    }
  });

  submitService.addEventListener("click", submitRecord);

  renderAll();
})();
</script>
ممكمم
TEXT - 2026-05-12 01:12:58
<!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>سامانه ثبت کارکرد</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;700;800;900&display=swap" rel="stylesheet"> <style> * { box-sizing: border-box; } html, body { margin: 0; padding: 0; direction: rtl; text-align: right; background: #eef2f7; } body, input, textarea, button, select { font-family: 'Vazirmatn', Tahoma, Arial, sans-serif; } body { min-height: 100vh; color: #1f2937; } .factory-app { min-height: 100vh; display: flex; justify-content: center; padding: 18px; } .factory-phone { width: 100%; max-width: 430px; background: #f8fafc; border-radius: 28px; overflow: hidden; box-shadow: 0 15px 40px rgba(15, 23, 42, 0.14); border: 1px solid #dbe3ef; position: relative; padding-bottom: 86px; } .app-header { background: linear-gradient(135deg, #0f766e, #1d4ed8); color: #fff; padding: 18px 18px 20px; display: flex; align-items: center; justify-content: space-between; } .app-header h1 { margin: 0 0 6px; font-size: 20px; font-weight: 800; } .app-header p { margin: 0; font-size: 12px; opacity: .9; } .demo-badge { background: rgba(255,255,255,.18); border: 1px solid rgba(255,255,255,.25); padding: 8px 12px; border-radius: 999px; font-size: 12px; font-weight: 700; backdrop-filter: blur(8px); } .pages-wrap { padding: 14px; } .page { display: none; animation: fadeIn .25s ease; } .page.active { display: block; } @keyframes fadeIn { from {opacity: 0; transform: translateY(8px);} to {opacity: 1; transform: translateY(0);} } .page-header, .page-title { margin-bottom: 14px; } .page-title h2, .page-header h1 { margin: 0 0 6px; font-size: 20px; font-weight: 800; color: #0f172a; } .page-title span, .page-subtitle { color: #64748b; font-size: 13px; } .search-card, .summary-card, .personal-record-card, .feedback-card, .form-card, .records-card, .wallet-card, .mini-card, .list-box, .manager-card, .stats-card, .chart-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 18px; box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05); } .search-card, .form-card, .records-card, .wallet-card, .list-box, .chart-card, .feedback-card, .personal-record-card { padding: 14px; margin-bottom: 14px; } .search-label { display: block; font-size: 13px; font-weight: 700; color: #334155; margin-bottom: 8px; } .search-input-wrap { display: flex; align-items: center; background: #f8fafc; border: 1px solid #dbe3ef; border-radius: 14px; padding: 0 12px; } .search-icon { font-size: 16px; margin-left: 8px; } input, textarea, button, select { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; } input, textarea, select { color: #0f172a; } .search-input-wrap input { height: 46px; } .service-results { margin-top: 10px; display: grid; gap: 8px; } .service-item { background: #f8fafc; border: 1px solid #dbe3ef; border-radius: 14px; padding: 12px; cursor: pointer; transition: .2s; } .service-item:hover { border-color: #60a5fa; background: #eff6ff; } .service-item strong { display: block; margin-bottom: 4px; font-size: 14px; } .service-item span { font-size: 12px; color: #64748b; } .summary-wrap, .mini-grid, .manager-grid, .stats-top-grid { display: grid; gap: 12px; margin-bottom: 14px; } .summary-wrap, .mini-grid { grid-template-columns: repeat(2, 1fr); } .mini-grid.three { grid-template-columns: repeat(3, 1fr); } .manager-grid { grid-template-columns: repeat(2, 1fr); } .stats-top-grid { grid-template-columns: repeat(2, 1fr); } .summary-card, .mini-card, .manager-card, .stats-card { padding: 14px; } .summary-card span, .mini-card small, .manager-card small, .stats-card small, .wallet-card small { display: block; color: #64748b; font-size: 12px; margin-bottom: 8px; } .summary-card strong, .mini-card strong, .manager-card strong, .stats-card strong, .wallet-card strong { font-size: 18px; font-weight: 800; color: #0f172a; } .wallet-card { display: flex; justify-content: space-between; align-items: center; } .personal-record-card, .feedback-card { display: flex; align-items: center; gap: 12px; } .record-icon { width: 48px; height: 48px; border-radius: 14px; background: linear-gradient(135deg, #f59e0b, #f97316); display: flex; align-items: center; justify-content: center; font-size: 24px; flex-shrink: 0; } .record-content span, .feedback-title { display: block; color: #64748b; font-size: 12px; margin-bottom: 4px; } .record-content strong, .feedback-main { display: block; font-size: 14px; font-weight: 800; color: #0f172a; margin-bottom: 4px; } .record-content small, .feedback-sub { color: #475569; font-size: 12px; } .feedback-card { justify-content: space-between; } .feedback-badge { min-width: 58px; text-align: center; padding: 10px 12px; border-radius: 14px; font-weight: 800; color: #fff; background: #10b981; flex-shrink: 0; } .feedback-badge.positive { background: linear-gradient(135deg, #10b981, #059669); } .selected-service { background: #f8fafc; border: 1px dashed #cbd5e1; border-radius: 14px; padding: 12px; margin-bottom: 12px; } .selected-service span { display: block; color: #64748b; font-size: 12px; margin-bottom: 4px; } .selected-service strong { font-size: 15px; color: #0f172a; } .form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 12px; } .field label { display: block; font-size: 12px; color: #475569; margin-bottom: 8px; font-weight: 700; } .field input, .field textarea { background: #f8fafc; border: 1px solid #dbe3ef; border-radius: 14px; padding: 12px; } textarea { min-height: 90px; resize: vertical; } .details-toggle { margin-bottom: 12px; background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; border-radius: 14px; padding: 12px; font-weight: 700; cursor: pointer; } .details-box { display: none; margin-bottom: 12px; } .details-box.open { display: block; } .submit-btn { background: linear-gradient(135deg, #0f766e, #1d4ed8); color: #fff; border-radius: 14px; padding: 14px; font-weight: 800; cursor: pointer; box-shadow: 0 8px 18px rgba(29, 78, 216, .2); } .records-title, .section-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; font-weight: 800; color: #0f172a; font-size: 14px; } #recordsCountText { color: #64748b; font-size: 12px; } .record-row, .list-row { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 14px; padding: 12px; margin-bottom: 10px; } .record-row-top, .list-row-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; } .record-row strong, .list-row strong { font-size: 14px; color: #0f172a; } .record-row span, .record-row small, .list-row span, .list-row small { color: #64748b; font-size: 12px; } .status-badge { padding: 6px 10px; border-radius: 999px; font-size: 11px; font-weight: 800; white-space: nowrap; } .status-pending { background: #fef3c7; color: #92400e; } .status-approved { background: #dcfce7; color: #166534; } .status-rejected { background: #fee2e2; color: #991b1b; } .empty-records, .empty-list { text-align: center; color: #64748b; font-size: 13px; padding: 18px; } .approve-actions { display: flex; gap: 8px; margin-top: 10px; } .approve-actions button { border-radius: 12px; padding: 10px; cursor: pointer; font-weight: 700; } .btn-approve { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; } .btn-reject { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; } .manager-card.blue { background: linear-gradient(135deg, #dbeafe, #bfdbfe); } .manager-card.violet { background: linear-gradient(135deg, #ede9fe, #ddd6fe); } .manager-card.orange { background: linear-gradient(135deg, #ffedd5, #fed7aa); } .manager-card.green { background: linear-gradient(135deg, #dcfce7, #bbf7d0); } .stats-card.navy { background: linear-gradient(135deg, #dbeafe, #93c5fd); } .stats-card.emerald { background: linear-gradient(135deg, #d1fae5, #6ee7b7); } .stats-card.violet { background: linear-gradient(135deg, #ede9fe, #c4b5fd); } .stats-card.orange { background: linear-gradient(135deg, #ffedd5, #fdba74); } .mini-card.warning { background: #fff7ed; } .mini-card.success { background: #ecfdf5; } .mini-card.danger { background: #fef2f2; } .bars-chart { display: flex; align-items: end; gap: 10px; min-height: 180px; overflow-x: auto; padding-top: 12px; } .bar-item { min-width: 54px; text-align: center; } .bar { width: 100%; border-radius: 12px 12px 6px 6px; background: linear-gradient(180deg, #1d4ed8, #0f766e); min-height: 10px; transition: .3s; } .bar-label { margin-top: 8px; font-size: 11px; color: #475569; } .bar-value { font-size: 11px; color: #0f172a; font-weight: 700; margin-bottom: 6px; } .days-strip { display: flex; flex-wrap: wrap; gap: 8px; } .day-pill { padding: 10px 12px; border-radius: 999px; background: #e0f2fe; color: #075985; font-size: 12px; font-weight: 700; border: 1px solid #bae6fd; } .bottom-nav { position: absolute; right: 0; left: 0; bottom: 0; background: rgba(255,255,255,.95); backdrop-filter: blur(10px); border-top: 1px solid #e5e7eb; display: grid; gap: 6px; padding: 10px; } .bottom-nav.five { grid-template-columns: repeat(5, 1fr); } .tab-btn { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 14px; padding: 10px 6px; font-size: 11px; color: #475569; cursor: pointer; font-weight: 700; } .tab-btn.active { background: linear-gradient(135deg, #0f766e, #1d4ed8); color: #fff; border-color: transparent; } .toast { position: fixed; bottom: 100px; right: 50%; transform: translateX(50%) translateY(20px); background: #0f172a; color: #fff; padding: 12px 16px; border-radius: 14px; font-size: 13px; opacity: 0; pointer-events: none; transition: .25s; z-index: 9999; white-space: nowrap; } .toast.show { opacity: 1; transform: translateX(50%) translateY(0); } @media (max-width: 480px) { .factory-app { padding: 0; } .factory-phone { max-width: 100%; min-height: 100vh; border-radius: 0; border: none; } .summary-wrap, .mini-grid, .manager-grid, .stats-top-grid, .form-grid { grid-template-columns: 1fr; } .mini-grid.three { grid-template-columns: 1fr; } .bottom-nav.five { grid-template-columns: repeat(3, 1fr); } } </style> </head> <body> <div class="factory-app" dir="rtl"> <div class="factory-phone"> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <div class="pages-wrap"> <section class="page active" id="page-register"> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage">هنوز رکورد جدیدی ثبت نشده</small> </div> </div> <div class="feedback-card"> <div> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> </div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مبلغ واحد</label> <input type="number" id="servicePrice" min="0" placeholder="مثلاً 250000" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> </section> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <script> const services = [ { id: 1, name: 'رنگ میز', defaultPrice: 250000, category: 'رنگ' }, { id: 2, name: 'جوشکاری', defaultPrice: 320000, category: 'فلزکاری' }, { id: 3, name: 'نجاری', defaultPrice: 280000, category: 'چوب' }, { id: 4, name: 'برش MDF', defaultPrice: 180000, category: 'برش' }, { id: 5, name: 'مونتاژ کمد', defaultPrice: 350000, category: 'مونتاژ' }, { id: 6, name: 'لبه چسبانی', defaultPrice: 120000, category: 'MDF' }, { id: 7, name: 'سوراخ‌کاری', defaultPrice: 90000, category: 'ماشین‌کاری' }, { id: 8, name: 'بسته‌بندی', defaultPrice: 70000, category: 'نهایی' } ]; let selectedService = null; let records = JSON.parse(localStorage.getItem('factory_records_demo') || '[]'); const serviceSearch = document.getElementById('serviceSearch'); const serviceResults = document.getElementById('serviceResults'); const selectedServiceName = document.getElementById('selectedServiceName'); const serviceCount = document.getElementById('serviceCount'); const servicePrice = document.getElementById('servicePrice'); const serviceDescription = document.getElementById('serviceDescription'); const submitService = document.getElementById('submitService'); const recordsList = document.getElementById('recordsList'); const todayAmount = document.getElementById('todayAmount'); const todayCount = document.getElementById('todayCount'); const recordsCountText = document.getElementById('recordsCountText'); const detailsToggle = document.getElementById('detailsToggle'); const detailsBox = document.getElementById('detailsBox'); const toast = document.getElementById('toast'); const workerTotalAmount = document.getElementById('workerTotalAmount'); const workerTotalCount = document.getElementById('workerTotalCount'); const approvedAmount = document.getElementById('approvedAmount'); const pendingAmount = document.getElementById('pendingAmount'); const workerAccountList = document.getElementById('workerAccountList'); const pendingCount = document.getElementById('pendingCount'); const approvedCount = document.getElementById('approvedCount'); const rejectedCount = document.getElementById('rejectedCount'); const approvalList = document.getElementById('approvalList'); const managerRecords = document.getElementById('managerRecords'); const managerAmount = document.getElementById('managerAmount'); const managerServices = document.getElementById('managerServices'); const managerServiceSummary = document.getElementById('managerServiceSummary'); const statsTodayAmount = document.getElementById('statsTodayAmount'); const statsTodayCount = document.getElementById('statsTodayCount'); const statsWeekAmount = document.getElementById('statsWeekAmount'); const statsWeekCount = document.getElementById('statsWeekCount'); const statsMonthAmount = document.getElementById('statsMonthAmount'); const statsMonthCount = document.getElementById('statsMonthCount'); const statsAllAmount = document.getElementById('statsAllAmount'); const statsAllCount = document.getElementById('statsAllCount'); const workedDaysCount = document.getElementById('workedDaysCount'); const avgDailyAmount = document.getElementById('avgDailyAmount'); const amountChart = document.getElementById('amountChart'); const workedDaysStrip = document.getElementById('workedDaysStrip'); const dailyStatsList = document.getElementById('dailyStatsList'); const bestRecordText = document.getElementById('bestRecordText'); const recordMessage = document.getElementById('recordMessage'); function toPersianNumber(value) { return String(value).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d]); } function formatMoney(value) { return toPersianNumber(Number(value || 0).toLocaleString('en-US')) + ' تومان'; } function getTodayKey(date = new Date()) { return date.toISOString().slice(0, 10); } function getJalaliLikeDate(date = new Date()) { try { return new Intl.DateTimeFormat('fa-IR').format(date); } catch (e) { return getTodayKey(date); } } function showToast(message) { toast.textContent = message; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2200); } function saveRecords() { localStorage.setItem('factory_records_demo', JSON.stringify(records)); } function renderServices(filter = '') { const q = filter.trim().toLowerCase(); const filtered = services.filter(s => s.name.toLowerCase().includes(q) || s.category.toLowerCase().includes(q) ); if (!filtered.length) { serviceResults.innerHTML = '<div class="empty-records">خدمتی پیدا نشد.</div>'; return; } serviceResults.innerHTML = filtered.map(service => ` <div class="service-item" data-id="${service.id}"> <strong>${service.name}</strong> <span>${service.category} • مبلغ پیشنهادی: ${formatMoney(service.defaultPrice)}</span> </div> `).join(''); document.querySelectorAll('.service-item').forEach(item => { item.addEventListener('click', () => { const id = Number(item.dataset.id); selectedService = services.find(s => s.id === id); selectedServiceName.textContent = selectedService.name; servicePrice.value = selectedService.defaultPrice; showToast('خدمت انتخاب شد'); }); }); } function updateTodaySummary() { const today = getTodayKey(); const todayRecords = records.filter(r => r.dateKey === today); const totalAmount = todayRecords.reduce((sum, r) => sum + r.total, 0); const totalCount = todayRecords.reduce((sum, r) => sum + r.count, 0); todayAmount.textContent = formatMoney(totalAmount); todayCount.textContent = toPersianNumber(totalCount); recordsCountText.textContent = toPersianNumber(todayRecords.length) + ' مورد'; if (!todayRecords.length) { recordsList.innerHTML = '<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>'; return; } recordsList.innerHTML = todayRecords.slice().reverse().map(record => ` <div class="record-row"> <div class="record-row-top"> <strong>${record.serviceName}</strong> <div class="status-badge ${statusClass(record.status)}">${statusLabel(record.status)}</div> </div> <div class="record-row-top"> <span>تعداد: ${toPersianNumber(record.count)}</span> <strong>${formatMoney(record.total)}</strong> </div> <small>${record.jalaliDate} • ${record.description ? record.description : 'بدون توضیح'}</small> </div> `).join(''); } function statusLabel(status) { if (status === 'approved') return 'تایید شده'; if (status === 'rejected') return 'رد شده'; return 'در انتظار'; } function statusClass(status) { if (status === 'approved') return 'status-approved'; if (status === 'rejected') return 'status-rejected'; return 'status-pending'; } function updateWorkerPage() { const totalAmountValue = records.reduce((sum, r) => sum + r.total, 0); const totalCountValue = records.reduce((sum, r) => sum + r.count, 0); const approvedValue = records.filter(r => r.status === 'approved').reduce((sum, r) => sum + r.total, 0); const pendingValue = records.filter(r => r.status === 'pending').reduce((sum, r) => sum + r.total, 0); workerTotalAmount.textContent = formatMoney(totalAmountValue); workerTotalCount.textContent = toPersianNumber(totalCountValue); approvedAmount.textContent = formatMoney(approvedValue); pendingAmount.textContent = formatMoney(pendingValue); if (!records.length) { workerAccountList.innerHTML = '<div class="empty-list">موردی وجود ندارد</div>'; return; } workerAccountList.innerHTML = records.slice().reverse().map(record => ` <div class="list-row"> <div class="list-row-top"> <strong>${record.serviceName}</strong> <div class="status-badge ${statusClass(record.status)}">${statusLabel(record.status)}</div> </div> <div class="list-row-top"> <span>${record.jalaliDate}</span> <strong>${formatMoney(record.total)}</strong> </div> <small>تعداد: ${toPersianNumber(record.count)} | مبلغ واحد: ${formatMoney(record.price)}</small> </div> `).join(''); } function updateApprovalPage() { const pendingItems = records.filter(r => r.status === 'pending'); const approvedItems = records.filter(r => r.status === 'approved'); const rejectedItems = records.filter(r => r.status === 'rejected'); pendingCount.textContent = toPersianNumber(pendingItems.length); approvedCount.textContent = toPersianNumber(approvedItems.length); rejectedCount.textContent = toPersianNumber(rejectedItems.length); if (!records.length) { approvalList.innerHTML = '<div class="empty-list">چیزی برای بررسی نیست</div>'; return; } approvalList.innerHTML = records.slice().reverse().map(record => ` <div class="list-row"> <div class="list-row-top"> <strong>${record.serviceName}</strong> <div class="status-badge ${statusClass(record.status)}">${statusLabel(record.status)}</div> </div> <div class="list-row-top"> <span>${record.jalaliDate}</span> <strong>${formatMoney(record.total)}</strong> </div> <small>تعداد: ${toPersianNumber(record.count)} | توضیح: ${record.description || '---'}</small> ${record.status === 'pending' ? ` <div class="approve-actions"> <button class="btn-approve" onclick="changeStatus(${record.id}, 'approved')">تایید</button> <button class="btn-reject" onclick="changeStatus(${record.id}, 'rejected')">رد</button> </div> ` : ''} </div> `).join(''); } window.changeStatus = function(id, status) { records = records.map(r => r.id === id ? { ...r, status } : r); saveRecords(); refreshAll(); showToast(status === 'approved' ? 'ثبت تایید شد' : 'ثبت رد شد'); } function updateManagerPage() { managerRecords.textContent = toPersianNumber(records.length); managerAmount.textContent = formatMoney(records.reduce((sum, r) => sum + r.total, 0)); managerServices.textContent = toPersianNumber(new Set(records.map(r => r.serviceName)).size); if (!records.length) { managerServiceSummary.innerHTML = '<div class="empty-list">هنوز آماری ثبت نشده</div>'; return; } const map = {}; records.forEach(r => { if (!map[r.serviceName]) { map[r.serviceName] = { count: 0, amount: 0 }; } map[r.serviceName].count += r.count; map[r.serviceName].amount += r.total; }); managerServiceSummary.innerHTML = Object.entries(map).map(([name, data]) => ` <div class="list-row"> <div class="list-row-top"> <strong>${name}</strong> <strong>${formatMoney(data.amount)}</strong> </div> <small>تعداد کل: ${toPersianNumber(data.count)}</small> </div> `).join(''); } function startOfWeek(date) { const d = new Date(date); const day = d.getDay(); const diff = day === 0 ? 6 : day - 1; d.setHours(0,0,0,0); d.setDate(d.getDate() - diff); return d; } function updateStatsPage() { const today = new Date(); const todayKey = getTodayKey(today); const weekStart = startOfWeek(today); const month = today.getMonth(); const year = today.getFullYear(); const todayItems = records.filter(r => r.dateKey === todayKey); const weekItems = records.filter(r => new Date(r.dateKey) >= weekStart); const monthItems = records.filter(r => { const d = new Date(r.dateKey); return d.getMonth() === month && d.getFullYear() === year; }); const allItems = records; statsTodayAmount.textContent = formatMoney(todayItems.reduce((s, r) => s + r.total, 0)); statsTodayCount.textContent = toPersianNumber(todayItems.length) + ' ثبت'; statsWeekAmount.textContent = formatMoney(weekItems.reduce((s, r) => s + r.total, 0)); statsWeekCount.textContent = toPersianNumber(weekItems.length) + ' ثبت'; statsMonthAmount.textContent = formatMoney(monthItems.reduce((s, r) => s + r.total, 0)); statsMonthCount.textContent = toPersianNumber(monthItems.length) + ' ثبت'; statsAllAmount.textContent = formatMoney(allItems.reduce((s, r) => s + r.total, 0)); statsAllCount.textContent = toPersianNumber(allItems.length) + ' ثبت'; const dayMap = {}; records.forEach(r => { if (!dayMap[r.dateKey]) { dayMap[r.dateKey] = { amount: 0, count: 0, jalaliDate: r.jalaliDate }; } dayMap[r.dateKey].amount += r.total; dayMap[r.dateKey].count += 1; }); const dayEntries = Object.entries(dayMap).sort((a, b) => a[0].localeCompare(b[0])); workedDaysCount.textContent = toPersianNumber(dayEntries.length) + ' روز'; const avg = dayEntries.length ? Math.round(dayEntries.reduce((s, [,v]) => s + v.amount, 0) / dayEntries.length) : 0; avgDailyAmount.textContent = formatMoney(avg); if (!dayEntries.length) { amountChart.innerHTML = '<div class="empty-list">داده‌ای برای نمودار وجود ندارد</div>'; workedDaysStrip.innerHTML = '<div class="empty-list">روز کاری ثبت نشده</div>'; dailyStatsList.innerHTML = '<div class="empty-list">آماری وجود ندارد</div>'; return; } const maxAmount = Math.max(...dayEntries.map(([,v]) => v.amount), 1); amountChart.innerHTML = dayEntries.map(([, value]) => { const height = Math.max(10, Math.round((value.amount / maxAmount) * 130)); return ` <div class="bar-item"> <div class="bar-value">${toPersianNumber(Math.round(value.amount / 1000))}k</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${value.jalaliDate}</div> </div> `; }).join(''); workedDaysStrip.innerHTML = dayEntries.map(([, value]) => `<div class="day-pill">${value.jalaliDate}</div>` ).join(''); dailyStatsList.innerHTML = dayEntries.slice().reverse().map(([, value]) => ` <div class="list-row"> <div class="list-row-top"> <strong>${value.jalaliDate}</strong> <strong>${formatMoney(value.amount)}</strong> </div> <small>تعداد ثبت: ${toPersianNumber(value.count)}</small> </div> `).join(''); } function updateBestRecord() { if (!records.length) { bestRecordText.textContent = 'هنوز رکوردی ثبت نشده'; recordMessage.textContent = 'اولین ثبتت را انجام بده'; return; } const dailyMap = {}; records.forEach(r => { if (!dailyMap[r.dateKey]) { dailyMap[r.dateKey] = { amount: 0, jalaliDate: r.jalaliDate }; } dailyMap[r.dateKey].amount += r.total; }); const best = Object.values(dailyMap).sort((a, b) => b.amount - a.amount)[0]; bestRecordText.textContent = `${formatMoney(best.amount)} در ${best.jalaliDate}`; const todayKey = getTodayKey(); const todayAmountValue = records .filter(r => r.dateKey === todayKey) .reduce((s, r) => s + r.total, 0); if (todayAmountValue > best.amount) { recordMessage.textContent = 'تبریک! امروز رکورد جدید ثبت کردی'; } else { const diff = best.amount - todayAmountValue; recordMessage.textContent = diff > 0 ? `${formatMoney(diff)} تا شکستن رکورد فاصله داری` : 'به رکوردت رسیده‌ای'; } } function refreshAll() { updateTodaySummary(); updateWorkerPage(); updateApprovalPage(); updateManagerPage(); updateStatsPage(); updateBestRecord(); } submitService.addEventListener('click', () => { if (!selectedService) { showToast('اول یک خدمت انتخاب کن'); return; } const count = Number(serviceCount.value || 0); const price = Number(servicePrice.value || 0); const description = serviceDescription.value.trim(); if (count <= 0) { showToast('تعداد معتبر وارد کن'); return; } if (price < 0) { showToast('مبلغ معتبر وارد کن'); return; } const now = new Date(); const record = { id: Date.now(), serviceId: selectedService.id, serviceName: selectedService.name, count, price, total: count * price, description, status: 'pending', dateKey: getTodayKey(now), jalaliDate: getJalaliLikeDate(now) }; records.push(record); saveRecords(); refreshAll(); serviceCount.value = 1; servicePrice.value = selectedService.defaultPrice; serviceDescription.value = ''; showToast('خدمت با موفقیت ثبت شد'); }); detailsToggle.addEventListener('click', () => { detailsBox.classList.toggle('open'); detailsToggle.textContent = detailsBox.classList.contains('open') ? 'بستن توضیحات اختیاری' : 'افزودن توضیحات اختیاری'; }); serviceSearch.addEventListener('input', e => { renderServices(e.target.value); }); document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const page = btn.dataset.page; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.getElementById('page-' + page).classList.add('active'); }); }); renderServices(); refreshAll(); </script> </body> </html>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>سامانه ثبت کارکرد</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;700;800;900&display=swap" rel="stylesheet">
  <style>
    * {
      box-sizing: border-box;
    }

    html, body {
      margin: 0;
      padding: 0;
      direction: rtl;
      text-align: right;
      background: #eef2f7;
    }

    body, input, textarea, button, select {
      font-family: 'Vazirmatn', Tahoma, Arial, sans-serif;
    }

    body {
      min-height: 100vh;
      color: #1f2937;
    }

    .factory-app {
      min-height: 100vh;
      display: flex;
      justify-content: center;
      padding: 18px;
    }

    .factory-phone {
      width: 100%;
      max-width: 430px;
      background: #f8fafc;
      border-radius: 28px;
      overflow: hidden;
      box-shadow: 0 15px 40px rgba(15, 23, 42, 0.14);
      border: 1px solid #dbe3ef;
      position: relative;
      padding-bottom: 86px;
    }

    .app-header {
      background: linear-gradient(135deg, #0f766e, #1d4ed8);
      color: #fff;
      padding: 18px 18px 20px;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }

    .app-header h1 {
      margin: 0 0 6px;
      font-size: 20px;
      font-weight: 800;
    }

    .app-header p {
      margin: 0;
      font-size: 12px;
      opacity: .9;
    }

    .demo-badge {
      background: rgba(255,255,255,.18);
      border: 1px solid rgba(255,255,255,.25);
      padding: 8px 12px;
      border-radius: 999px;
      font-size: 12px;
      font-weight: 700;
      backdrop-filter: blur(8px);
    }

    .pages-wrap {
      padding: 14px;
    }

    .page {
      display: none;
      animation: fadeIn .25s ease;
    }

    .page.active {
      display: block;
    }

    @keyframes fadeIn {
      from {opacity: 0; transform: translateY(8px);}
      to {opacity: 1; transform: translateY(0);}
    }

    .page-header,
    .page-title {
      margin-bottom: 14px;
    }

    .page-title h2,
    .page-header h1 {
      margin: 0 0 6px;
      font-size: 20px;
      font-weight: 800;
      color: #0f172a;
    }

    .page-title span,
    .page-subtitle {
      color: #64748b;
      font-size: 13px;
    }

    .search-card,
    .summary-card,
    .personal-record-card,
    .feedback-card,
    .form-card,
    .records-card,
    .wallet-card,
    .mini-card,
    .list-box,
    .manager-card,
    .stats-card,
    .chart-card {
      background: #fff;
      border: 1px solid #e5e7eb;
      border-radius: 18px;
      box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
    }

    .search-card,
    .form-card,
    .records-card,
    .wallet-card,
    .list-box,
    .chart-card,
    .feedback-card,
    .personal-record-card {
      padding: 14px;
      margin-bottom: 14px;
    }

    .search-label {
      display: block;
      font-size: 13px;
      font-weight: 700;
      color: #334155;
      margin-bottom: 8px;
    }

    .search-input-wrap {
      display: flex;
      align-items: center;
      background: #f8fafc;
      border: 1px solid #dbe3ef;
      border-radius: 14px;
      padding: 0 12px;
    }

    .search-icon {
      font-size: 16px;
      margin-left: 8px;
    }

    input, textarea, button, select {
      width: 100%;
      border: none;
      outline: none;
      background: transparent;
      font-size: 14px;
    }

    input, textarea, select {
      color: #0f172a;
    }

    .search-input-wrap input {
      height: 46px;
    }

    .service-results {
      margin-top: 10px;
      display: grid;
      gap: 8px;
    }

    .service-item {
      background: #f8fafc;
      border: 1px solid #dbe3ef;
      border-radius: 14px;
      padding: 12px;
      cursor: pointer;
      transition: .2s;
    }

    .service-item:hover {
      border-color: #60a5fa;
      background: #eff6ff;
    }

    .service-item strong {
      display: block;
      margin-bottom: 4px;
      font-size: 14px;
    }

    .service-item span {
      font-size: 12px;
      color: #64748b;
    }

    .summary-wrap,
    .mini-grid,
    .manager-grid,
    .stats-top-grid {
      display: grid;
      gap: 12px;
      margin-bottom: 14px;
    }

    .summary-wrap,
    .mini-grid {
      grid-template-columns: repeat(2, 1fr);
    }

    .mini-grid.three {
      grid-template-columns: repeat(3, 1fr);
    }

    .manager-grid {
      grid-template-columns: repeat(2, 1fr);
    }

    .stats-top-grid {
      grid-template-columns: repeat(2, 1fr);
    }

    .summary-card,
    .mini-card,
    .manager-card,
    .stats-card {
      padding: 14px;
    }

    .summary-card span,
    .mini-card small,
    .manager-card small,
    .stats-card small,
    .wallet-card small {
      display: block;
      color: #64748b;
      font-size: 12px;
      margin-bottom: 8px;
    }

    .summary-card strong,
    .mini-card strong,
    .manager-card strong,
    .stats-card strong,
    .wallet-card strong {
      font-size: 18px;
      font-weight: 800;
      color: #0f172a;
    }

    .wallet-card {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .personal-record-card,
    .feedback-card {
      display: flex;
      align-items: center;
      gap: 12px;
    }

    .record-icon {
      width: 48px;
      height: 48px;
      border-radius: 14px;
      background: linear-gradient(135deg, #f59e0b, #f97316);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 24px;
      flex-shrink: 0;
    }

    .record-content span,
    .feedback-title {
      display: block;
      color: #64748b;
      font-size: 12px;
      margin-bottom: 4px;
    }

    .record-content strong,
    .feedback-main {
      display: block;
      font-size: 14px;
      font-weight: 800;
      color: #0f172a;
      margin-bottom: 4px;
    }

    .record-content small,
    .feedback-sub {
      color: #475569;
      font-size: 12px;
    }

    .feedback-card {
      justify-content: space-between;
    }

    .feedback-badge {
      min-width: 58px;
      text-align: center;
      padding: 10px 12px;
      border-radius: 14px;
      font-weight: 800;
      color: #fff;
      background: #10b981;
      flex-shrink: 0;
    }

    .feedback-badge.positive {
      background: linear-gradient(135deg, #10b981, #059669);
    }

    .selected-service {
      background: #f8fafc;
      border: 1px dashed #cbd5e1;
      border-radius: 14px;
      padding: 12px;
      margin-bottom: 12px;
    }

    .selected-service span {
      display: block;
      color: #64748b;
      font-size: 12px;
      margin-bottom: 4px;
    }

    .selected-service strong {
      font-size: 15px;
      color: #0f172a;
    }

    .form-grid {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 12px;
      margin-bottom: 12px;
    }

    .field label {
      display: block;
      font-size: 12px;
      color: #475569;
      margin-bottom: 8px;
      font-weight: 700;
    }

    .field input,
    .field textarea {
      background: #f8fafc;
      border: 1px solid #dbe3ef;
      border-radius: 14px;
      padding: 12px;
    }

    textarea {
      min-height: 90px;
      resize: vertical;
    }

    .details-toggle {
      margin-bottom: 12px;
      background: #eff6ff;
      color: #1d4ed8;
      border: 1px solid #bfdbfe;
      border-radius: 14px;
      padding: 12px;
      font-weight: 700;
      cursor: pointer;
    }

    .details-box {
      display: none;
      margin-bottom: 12px;
    }

    .details-box.open {
      display: block;
    }

    .submit-btn {
      background: linear-gradient(135deg, #0f766e, #1d4ed8);
      color: #fff;
      border-radius: 14px;
      padding: 14px;
      font-weight: 800;
      cursor: pointer;
      box-shadow: 0 8px 18px rgba(29, 78, 216, .2);
    }

    .records-title,
    .section-title {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 10px;
      font-weight: 800;
      color: #0f172a;
      font-size: 14px;
    }

    #recordsCountText {
      color: #64748b;
      font-size: 12px;
    }

    .record-row,
    .list-row {
      background: #f8fafc;
      border: 1px solid #e2e8f0;
      border-radius: 14px;
      padding: 12px;
      margin-bottom: 10px;
    }

    .record-row-top,
    .list-row-top {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      margin-bottom: 8px;
    }

    .record-row strong,
    .list-row strong {
      font-size: 14px;
      color: #0f172a;
    }

    .record-row span,
    .record-row small,
    .list-row span,
    .list-row small {
      color: #64748b;
      font-size: 12px;
    }

    .status-badge {
      padding: 6px 10px;
      border-radius: 999px;
      font-size: 11px;
      font-weight: 800;
      white-space: nowrap;
    }

    .status-pending {
      background: #fef3c7;
      color: #92400e;
    }

    .status-approved {
      background: #dcfce7;
      color: #166534;
    }

    .status-rejected {
      background: #fee2e2;
      color: #991b1b;
    }

    .empty-records,
    .empty-list {
      text-align: center;
      color: #64748b;
      font-size: 13px;
      padding: 18px;
    }

    .approve-actions {
      display: flex;
      gap: 8px;
      margin-top: 10px;
    }

    .approve-actions button {
      border-radius: 12px;
      padding: 10px;
      cursor: pointer;
      font-weight: 700;
    }

    .btn-approve {
      background: #dcfce7;
      color: #166534;
      border: 1px solid #bbf7d0;
    }

    .btn-reject {
      background: #fee2e2;
      color: #991b1b;
      border: 1px solid #fecaca;
    }

    .manager-card.blue { background: linear-gradient(135deg, #dbeafe, #bfdbfe); }
    .manager-card.violet { background: linear-gradient(135deg, #ede9fe, #ddd6fe); }
    .manager-card.orange { background: linear-gradient(135deg, #ffedd5, #fed7aa); }
    .manager-card.green { background: linear-gradient(135deg, #dcfce7, #bbf7d0); }

    .stats-card.navy { background: linear-gradient(135deg, #dbeafe, #93c5fd); }
    .stats-card.emerald { background: linear-gradient(135deg, #d1fae5, #6ee7b7); }
    .stats-card.violet { background: linear-gradient(135deg, #ede9fe, #c4b5fd); }
    .stats-card.orange { background: linear-gradient(135deg, #ffedd5, #fdba74); }

    .mini-card.warning { background: #fff7ed; }
    .mini-card.success { background: #ecfdf5; }
    .mini-card.danger { background: #fef2f2; }

    .bars-chart {
      display: flex;
      align-items: end;
      gap: 10px;
      min-height: 180px;
      overflow-x: auto;
      padding-top: 12px;
    }

    .bar-item {
      min-width: 54px;
      text-align: center;
    }

    .bar {
      width: 100%;
      border-radius: 12px 12px 6px 6px;
      background: linear-gradient(180deg, #1d4ed8, #0f766e);
      min-height: 10px;
      transition: .3s;
    }

    .bar-label {
      margin-top: 8px;
      font-size: 11px;
      color: #475569;
    }

    .bar-value {
      font-size: 11px;
      color: #0f172a;
      font-weight: 700;
      margin-bottom: 6px;
    }

    .days-strip {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }

    .day-pill {
      padding: 10px 12px;
      border-radius: 999px;
      background: #e0f2fe;
      color: #075985;
      font-size: 12px;
      font-weight: 700;
      border: 1px solid #bae6fd;
    }

    .bottom-nav {
      position: absolute;
      right: 0;
      left: 0;
      bottom: 0;
      background: rgba(255,255,255,.95);
      backdrop-filter: blur(10px);
      border-top: 1px solid #e5e7eb;
      display: grid;
      gap: 6px;
      padding: 10px;
    }

    .bottom-nav.five {
      grid-template-columns: repeat(5, 1fr);
    }

    .tab-btn {
      background: #f8fafc;
      border: 1px solid #e5e7eb;
      border-radius: 14px;
      padding: 10px 6px;
      font-size: 11px;
      color: #475569;
      cursor: pointer;
      font-weight: 700;
    }

    .tab-btn.active {
      background: linear-gradient(135deg, #0f766e, #1d4ed8);
      color: #fff;
      border-color: transparent;
    }

    .toast {
      position: fixed;
      bottom: 100px;
      right: 50%;
      transform: translateX(50%) translateY(20px);
      background: #0f172a;
      color: #fff;
      padding: 12px 16px;
      border-radius: 14px;
      font-size: 13px;
      opacity: 0;
      pointer-events: none;
      transition: .25s;
      z-index: 9999;
      white-space: nowrap;
    }

    .toast.show {
      opacity: 1;
      transform: translateX(50%) translateY(0);
    }

    @media (max-width: 480px) {
      .factory-app {
        padding: 0;
      }

      .factory-phone {
        max-width: 100%;
        min-height: 100vh;
        border-radius: 0;
        border: none;
      }

      .summary-wrap,
      .mini-grid,
      .manager-grid,
      .stats-top-grid,
      .form-grid {
        grid-template-columns: 1fr;
      }

      .mini-grid.three {
        grid-template-columns: 1fr;
      }

      .bottom-nav.five {
        grid-template-columns: repeat(3, 1fr);
      }
    }
  </style>
</head>
<body>

<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <div class="pages-wrap">
      <section class="page active" id="page-register">
        <div class="worker-page">
          <div class="page-header">
            <h1 class="page-title">ثبت کار امروز</h1>
            <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
          </div>

          <div class="search-card">
            <label class="search-label">جستجوی خدمت</label>
            <div class="search-input-wrap">
              <div class="search-icon">🔍</div>
              <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
            </div>
            <div class="service-results" id="serviceResults"></div>
          </div>

          <div class="summary-wrap">
            <div class="summary-card">
              <span>مبلغ امروز</span>
              <strong id="todayAmount">۰ تومان</strong>
            </div>
            <div class="summary-card">
              <span>تعداد امروز</span>
              <strong id="todayCount">۰</strong>
            </div>
          </div>

          <div class="personal-record-card">
            <div class="record-icon">🏆</div>
            <div class="record-content">
              <span>رکورد روزانه تو</span>
              <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
              <small id="recordMessage">هنوز رکورد جدیدی ثبت نشده</small>
            </div>
          </div>

          <div class="feedback-card">
            <div>
              <div class="feedback-title">آخرین بازخورد عملکرد</div>
              <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
              <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
            </div>
            <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
          </div>

          <div class="form-card" id="serviceForm">
            <div class="selected-service">
              <span>خدمت انتخاب شده</span>
              <strong id="selectedServiceName">---</strong>
            </div>

            <div class="form-grid">
              <div class="field">
                <label>تعداد</label>
                <input type="number" id="serviceCount" min="1" value="1" />
              </div>
              <div class="field">
                <label>مبلغ واحد</label>
                <input type="number" id="servicePrice" min="0" placeholder="مثلاً 250000" />
              </div>
            </div>

            <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

            <div class="details-box" id="detailsBox">
              <div class="field">
                <label>توضیحات</label>
                <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
              </div>
            </div>

            <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
          </div>

          <div class="records-card">
            <div class="records-title">
              <strong>ثبت‌های امروز</strong>
              <span id="recordsCountText">۰ مورد</span>
            </div>
            <div id="recordsList">
              <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
            </div>
          </div>
        </div>
      </section>

      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<script>
  const services = [
    { id: 1, name: 'رنگ میز', defaultPrice: 250000, category: 'رنگ' },
    { id: 2, name: 'جوشکاری', defaultPrice: 320000, category: 'فلزکاری' },
    { id: 3, name: 'نجاری', defaultPrice: 280000, category: 'چوب' },
    { id: 4, name: 'برش MDF', defaultPrice: 180000, category: 'برش' },
    { id: 5, name: 'مونتاژ کمد', defaultPrice: 350000, category: 'مونتاژ' },
    { id: 6, name: 'لبه چسبانی', defaultPrice: 120000, category: 'MDF' },
    { id: 7, name: 'سوراخ‌کاری', defaultPrice: 90000, category: 'ماشین‌کاری' },
    { id: 8, name: 'بسته‌بندی', defaultPrice: 70000, category: 'نهایی' }
  ];

  let selectedService = null;
  let records = JSON.parse(localStorage.getItem('factory_records_demo') || '[]');

  const serviceSearch = document.getElementById('serviceSearch');
  const serviceResults = document.getElementById('serviceResults');
  const selectedServiceName = document.getElementById('selectedServiceName');
  const serviceCount = document.getElementById('serviceCount');
  const servicePrice = document.getElementById('servicePrice');
  const serviceDescription = document.getElementById('serviceDescription');
  const submitService = document.getElementById('submitService');
  const recordsList = document.getElementById('recordsList');
  const todayAmount = document.getElementById('todayAmount');
  const todayCount = document.getElementById('todayCount');
  const recordsCountText = document.getElementById('recordsCountText');
  const detailsToggle = document.getElementById('detailsToggle');
  const detailsBox = document.getElementById('detailsBox');
  const toast = document.getElementById('toast');

  const workerTotalAmount = document.getElementById('workerTotalAmount');
  const workerTotalCount = document.getElementById('workerTotalCount');
  const approvedAmount = document.getElementById('approvedAmount');
  const pendingAmount = document.getElementById('pendingAmount');
  const workerAccountList = document.getElementById('workerAccountList');

  const pendingCount = document.getElementById('pendingCount');
  const approvedCount = document.getElementById('approvedCount');
  const rejectedCount = document.getElementById('rejectedCount');
  const approvalList = document.getElementById('approvalList');

  const managerRecords = document.getElementById('managerRecords');
  const managerAmount = document.getElementById('managerAmount');
  const managerServices = document.getElementById('managerServices');
  const managerServiceSummary = document.getElementById('managerServiceSummary');

  const statsTodayAmount = document.getElementById('statsTodayAmount');
  const statsTodayCount = document.getElementById('statsTodayCount');
  const statsWeekAmount = document.getElementById('statsWeekAmount');
  const statsWeekCount = document.getElementById('statsWeekCount');
  const statsMonthAmount = document.getElementById('statsMonthAmount');
  const statsMonthCount = document.getElementById('statsMonthCount');
  const statsAllAmount = document.getElementById('statsAllAmount');
  const statsAllCount = document.getElementById('statsAllCount');
  const workedDaysCount = document.getElementById('workedDaysCount');
  const avgDailyAmount = document.getElementById('avgDailyAmount');
  const amountChart = document.getElementById('amountChart');
  const workedDaysStrip = document.getElementById('workedDaysStrip');
  const dailyStatsList = document.getElementById('dailyStatsList');

  const bestRecordText = document.getElementById('bestRecordText');
  const recordMessage = document.getElementById('recordMessage');

  function toPersianNumber(value) {
    return String(value).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d]);
  }

  function formatMoney(value) {
    return toPersianNumber(Number(value || 0).toLocaleString('en-US')) + ' تومان';
  }

  function getTodayKey(date = new Date()) {
    return date.toISOString().slice(0, 10);
  }

  function getJalaliLikeDate(date = new Date()) {
    try {
      return new Intl.DateTimeFormat('fa-IR').format(date);
    } catch (e) {
      return getTodayKey(date);
    }
  }

  function showToast(message) {
    toast.textContent = message;
    toast.classList.add('show');
    setTimeout(() => toast.classList.remove('show'), 2200);
  }

  function saveRecords() {
    localStorage.setItem('factory_records_demo', JSON.stringify(records));
  }

  function renderServices(filter = '') {
    const q = filter.trim().toLowerCase();
    const filtered = services.filter(s =>
      s.name.toLowerCase().includes(q) || s.category.toLowerCase().includes(q)
    );

    if (!filtered.length) {
      serviceResults.innerHTML = '<div class="empty-records">خدمتی پیدا نشد.</div>';
      return;
    }

    serviceResults.innerHTML = filtered.map(service => `
      <div class="service-item" data-id="${service.id}">
        <strong>${service.name}</strong>
        <span>${service.category} • مبلغ پیشنهادی: ${formatMoney(service.defaultPrice)}</span>
      </div>
    `).join('');

    document.querySelectorAll('.service-item').forEach(item => {
      item.addEventListener('click', () => {
        const id = Number(item.dataset.id);
        selectedService = services.find(s => s.id === id);
        selectedServiceName.textContent = selectedService.name;
        servicePrice.value = selectedService.defaultPrice;
        showToast('خدمت انتخاب شد');
      });
    });
  }

  function updateTodaySummary() {
    const today = getTodayKey();
    const todayRecords = records.filter(r => r.dateKey === today);
    const totalAmount = todayRecords.reduce((sum, r) => sum + r.total, 0);
    const totalCount = todayRecords.reduce((sum, r) => sum + r.count, 0);

    todayAmount.textContent = formatMoney(totalAmount);
    todayCount.textContent = toPersianNumber(totalCount);
    recordsCountText.textContent = toPersianNumber(todayRecords.length) + ' مورد';

    if (!todayRecords.length) {
      recordsList.innerHTML = '<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>';
      return;
    }

    recordsList.innerHTML = todayRecords.slice().reverse().map(record => `
      <div class="record-row">
        <div class="record-row-top">
          <strong>${record.serviceName}</strong>
          <div class="status-badge ${statusClass(record.status)}">${statusLabel(record.status)}</div>
        </div>
        <div class="record-row-top">
          <span>تعداد: ${toPersianNumber(record.count)}</span>
          <strong>${formatMoney(record.total)}</strong>
        </div>
        <small>${record.jalaliDate} • ${record.description ? record.description : 'بدون توضیح'}</small>
      </div>
    `).join('');
  }

  function statusLabel(status) {
    if (status === 'approved') return 'تایید شده';
    if (status === 'rejected') return 'رد شده';
    return 'در انتظار';
  }

  function statusClass(status) {
    if (status === 'approved') return 'status-approved';
    if (status === 'rejected') return 'status-rejected';
    return 'status-pending';
  }

  function updateWorkerPage() {
    const totalAmountValue = records.reduce((sum, r) => sum + r.total, 0);
    const totalCountValue = records.reduce((sum, r) => sum + r.count, 0);
    const approvedValue = records.filter(r => r.status === 'approved').reduce((sum, r) => sum + r.total, 0);
    const pendingValue = records.filter(r => r.status === 'pending').reduce((sum, r) => sum + r.total, 0);

    workerTotalAmount.textContent = formatMoney(totalAmountValue);
    workerTotalCount.textContent = toPersianNumber(totalCountValue);
    approvedAmount.textContent = formatMoney(approvedValue);
    pendingAmount.textContent = formatMoney(pendingValue);

    if (!records.length) {
      workerAccountList.innerHTML = '<div class="empty-list">موردی وجود ندارد</div>';
      return;
    }

    workerAccountList.innerHTML = records.slice().reverse().map(record => `
      <div class="list-row">
        <div class="list-row-top">
          <strong>${record.serviceName}</strong>
          <div class="status-badge ${statusClass(record.status)}">${statusLabel(record.status)}</div>
        </div>
        <div class="list-row-top">
          <span>${record.jalaliDate}</span>
          <strong>${formatMoney(record.total)}</strong>
        </div>
        <small>تعداد: ${toPersianNumber(record.count)} | مبلغ واحد: ${formatMoney(record.price)}</small>
      </div>
    `).join('');
  }

  function updateApprovalPage() {
    const pendingItems = records.filter(r => r.status === 'pending');
    const approvedItems = records.filter(r => r.status === 'approved');
    const rejectedItems = records.filter(r => r.status === 'rejected');

    pendingCount.textContent = toPersianNumber(pendingItems.length);
    approvedCount.textContent = toPersianNumber(approvedItems.length);
    rejectedCount.textContent = toPersianNumber(rejectedItems.length);

    if (!records.length) {
      approvalList.innerHTML = '<div class="empty-list">چیزی برای بررسی نیست</div>';
      return;
    }

    approvalList.innerHTML = records.slice().reverse().map(record => `
      <div class="list-row">
        <div class="list-row-top">
          <strong>${record.serviceName}</strong>
          <div class="status-badge ${statusClass(record.status)}">${statusLabel(record.status)}</div>
        </div>
        <div class="list-row-top">
          <span>${record.jalaliDate}</span>
          <strong>${formatMoney(record.total)}</strong>
        </div>
        <small>تعداد: ${toPersianNumber(record.count)} | توضیح: ${record.description || '---'}</small>
        ${record.status === 'pending' ? `
          <div class="approve-actions">
            <button class="btn-approve" onclick="changeStatus(${record.id}, 'approved')">تایید</button>
            <button class="btn-reject" onclick="changeStatus(${record.id}, 'rejected')">رد</button>
          </div>
        ` : ''}
      </div>
    `).join('');
  }

  window.changeStatus = function(id, status) {
    records = records.map(r => r.id === id ? { ...r, status } : r);
    saveRecords();
    refreshAll();
    showToast(status === 'approved' ? 'ثبت تایید شد' : 'ثبت رد شد');
  }

  function updateManagerPage() {
    managerRecords.textContent = toPersianNumber(records.length);
    managerAmount.textContent = formatMoney(records.reduce((sum, r) => sum + r.total, 0));
    managerServices.textContent = toPersianNumber(new Set(records.map(r => r.serviceName)).size);

    if (!records.length) {
      managerServiceSummary.innerHTML = '<div class="empty-list">هنوز آماری ثبت نشده</div>';
      return;
    }

    const map = {};
    records.forEach(r => {
      if (!map[r.serviceName]) {
        map[r.serviceName] = { count: 0, amount: 0 };
      }
      map[r.serviceName].count += r.count;
      map[r.serviceName].amount += r.total;
    });

    managerServiceSummary.innerHTML = Object.entries(map).map(([name, data]) => `
      <div class="list-row">
        <div class="list-row-top">
          <strong>${name}</strong>
          <strong>${formatMoney(data.amount)}</strong>
        </div>
        <small>تعداد کل: ${toPersianNumber(data.count)}</small>
      </div>
    `).join('');
  }

  function startOfWeek(date) {
    const d = new Date(date);
    const day = d.getDay();
    const diff = day === 0 ? 6 : day - 1;
    d.setHours(0,0,0,0);
    d.setDate(d.getDate() - diff);
    return d;
  }

  function updateStatsPage() {
    const today = new Date();
    const todayKey = getTodayKey(today);

    const weekStart = startOfWeek(today);
    const month = today.getMonth();
    const year = today.getFullYear();

    const todayItems = records.filter(r => r.dateKey === todayKey);
    const weekItems = records.filter(r => new Date(r.dateKey) >= weekStart);
    const monthItems = records.filter(r => {
      const d = new Date(r.dateKey);
      return d.getMonth() === month && d.getFullYear() === year;
    });
    const allItems = records;

    statsTodayAmount.textContent = formatMoney(todayItems.reduce((s, r) => s + r.total, 0));
    statsTodayCount.textContent = toPersianNumber(todayItems.length) + ' ثبت';

    statsWeekAmount.textContent = formatMoney(weekItems.reduce((s, r) => s + r.total, 0));
    statsWeekCount.textContent = toPersianNumber(weekItems.length) + ' ثبت';

    statsMonthAmount.textContent = formatMoney(monthItems.reduce((s, r) => s + r.total, 0));
    statsMonthCount.textContent = toPersianNumber(monthItems.length) + ' ثبت';

    statsAllAmount.textContent = formatMoney(allItems.reduce((s, r) => s + r.total, 0));
    statsAllCount.textContent = toPersianNumber(allItems.length) + ' ثبت';

    const dayMap = {};
    records.forEach(r => {
      if (!dayMap[r.dateKey]) {
        dayMap[r.dateKey] = {
          amount: 0,
          count: 0,
          jalaliDate: r.jalaliDate
        };
      }
      dayMap[r.dateKey].amount += r.total;
      dayMap[r.dateKey].count += 1;
    });

    const dayEntries = Object.entries(dayMap).sort((a, b) => a[0].localeCompare(b[0]));
    workedDaysCount.textContent = toPersianNumber(dayEntries.length) + ' روز';

    const avg = dayEntries.length
      ? Math.round(dayEntries.reduce((s, [,v]) => s + v.amount, 0) / dayEntries.length)
      : 0;
    avgDailyAmount.textContent = formatMoney(avg);

    if (!dayEntries.length) {
      amountChart.innerHTML = '<div class="empty-list">داده‌ای برای نمودار وجود ندارد</div>';
      workedDaysStrip.innerHTML = '<div class="empty-list">روز کاری ثبت نشده</div>';
      dailyStatsList.innerHTML = '<div class="empty-list">آماری وجود ندارد</div>';
      return;
    }

    const maxAmount = Math.max(...dayEntries.map(([,v]) => v.amount), 1);

    amountChart.innerHTML = dayEntries.map(([, value]) => {
      const height = Math.max(10, Math.round((value.amount / maxAmount) * 130));
      return `
        <div class="bar-item">
          <div class="bar-value">${toPersianNumber(Math.round(value.amount / 1000))}k</div>
          <div class="bar" style="height:${height}px"></div>
          <div class="bar-label">${value.jalaliDate}</div>
        </div>
      `;
    }).join('');

    workedDaysStrip.innerHTML = dayEntries.map(([, value]) =>
      `<div class="day-pill">${value.jalaliDate}</div>`
    ).join('');

    dailyStatsList.innerHTML = dayEntries.slice().reverse().map(([, value]) => `
      <div class="list-row">
        <div class="list-row-top">
          <strong>${value.jalaliDate}</strong>
          <strong>${formatMoney(value.amount)}</strong>
        </div>
        <small>تعداد ثبت: ${toPersianNumber(value.count)}</small>
      </div>
    `).join('');
  }

  function updateBestRecord() {
    if (!records.length) {
      bestRecordText.textContent = 'هنوز رکوردی ثبت نشده';
      recordMessage.textContent = 'اولین ثبتت را انجام بده';
      return;
    }

    const dailyMap = {};
    records.forEach(r => {
      if (!dailyMap[r.dateKey]) {
        dailyMap[r.dateKey] = { amount: 0, jalaliDate: r.jalaliDate };
      }
      dailyMap[r.dateKey].amount += r.total;
    });

    const best = Object.values(dailyMap).sort((a, b) => b.amount - a.amount)[0];
    bestRecordText.textContent = `${formatMoney(best.amount)} در ${best.jalaliDate}`;

    const todayKey = getTodayKey();
    const todayAmountValue = records
      .filter(r => r.dateKey === todayKey)
      .reduce((s, r) => s + r.total, 0);

    if (todayAmountValue > best.amount) {
      recordMessage.textContent = 'تبریک! امروز رکورد جدید ثبت کردی';
    } else {
      const diff = best.amount - todayAmountValue;
      recordMessage.textContent = diff > 0
        ? `${formatMoney(diff)} تا شکستن رکورد فاصله داری`
        : 'به رکوردت رسیده‌ای';
    }
  }

  function refreshAll() {
    updateTodaySummary();
    updateWorkerPage();
    updateApprovalPage();
    updateManagerPage();
    updateStatsPage();
    updateBestRecord();
  }

  submitService.addEventListener('click', () => {
    if (!selectedService) {
      showToast('اول یک خدمت انتخاب کن');
      return;
    }

    const count = Number(serviceCount.value || 0);
    const price = Number(servicePrice.value || 0);
    const description = serviceDescription.value.trim();

    if (count <= 0) {
      showToast('تعداد معتبر وارد کن');
      return;
    }

    if (price < 0) {
      showToast('مبلغ معتبر وارد کن');
      return;
    }

    const now = new Date();
    const record = {
      id: Date.now(),
      serviceId: selectedService.id,
      serviceName: selectedService.name,
      count,
      price,
      total: count * price,
      description,
      status: 'pending',
      dateKey: getTodayKey(now),
      jalaliDate: getJalaliLikeDate(now)
    };

    records.push(record);
    saveRecords();
    refreshAll();

    serviceCount.value = 1;
    servicePrice.value = selectedService.defaultPrice;
    serviceDescription.value = '';

    showToast('خدمت با موفقیت ثبت شد');
  });

  detailsToggle.addEventListener('click', () => {
    detailsBox.classList.toggle('open');
    detailsToggle.textContent = detailsBox.classList.contains('open')
      ? 'بستن توضیحات اختیاری'
      : 'افزودن توضیحات اختیاری';
  });

  serviceSearch.addEventListener('input', e => {
    renderServices(e.target.value);
  });

  document.querySelectorAll('.tab-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const page = btn.dataset.page;

      document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');

      document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
      document.getElementById('page-' + page).classList.add('active');
    });
  });

  renderServices();
  refreshAll();
</script>

</body>
</html>
۱۱۱۱۲
TEXT - 2026-05-12 01:04:08
add_shortcode('factory_app', function () { ob_start(); ?> <div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage"></small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } * { box-sizing: border-box; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-wrap .summary-card { background: #ffffff; color: #111827; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-wrap .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-wrap .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: none; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } @media (max-width: 380px) { .summary-wrap .summary-card strong { font-size: 14px; } } </style> <script> (function(){ let selectedService = null; let records = []; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const toast = document.getElementById("toast"); const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); } function updatePersonalRecord() { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = ""; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); showToast("ثبت جدید اضافه شد"); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmountValue); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderRecords(); updateSummary(); updatePersonalRecord(); renderFeedback(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); renderAll(); })(); </script> <?php return ob_get_clean(); });
add_shortcode('factory_app', function () {
    ob_start();
    ?>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="worker-page">

          <div class="page-header">
            <h1 class="page-title">ثبت کار امروز</h1>
            <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
          </div>

          <div class="search-card">
            <label class="search-label">جستجوی خدمت</label>

            <div class="search-input-wrap">
              <div class="search-icon">🔍</div>
              <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
            </div>

            <div class="service-results" id="serviceResults"></div>
          </div>

          <div class="summary-wrap">
            <div class="summary-card">
              <span>مبلغ امروز</span>
              <strong id="todayAmount">۰ تومان</strong>
            </div>

            <div class="summary-card">
              <span>تعداد امروز</span>
              <strong id="todayCount">۰</strong>
            </div>
          </div>

          <div class="personal-record-card">
            <div class="record-icon">🏆</div>
            <div class="record-content">
              <span>رکورد روزانه تو</span>
              <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
              <small id="recordMessage"></small>
            </div>
          </div>

          <div class="feedback-card">
            <div class="feedback-title">آخرین بازخورد عملکرد</div>
            <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
            <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
            <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
          </div>

          <div class="form-card" id="serviceForm">
            <div class="selected-service">
              <span>خدمت انتخاب شده</span>
              <strong id="selectedServiceName">---</strong>
            </div>

            <div class="form-grid">
              <div class="field">
                <label>تعداد</label>
                <input type="number" id="serviceCount" min="1" value="1" />
              </div>

              <div class="field">
                <label>مقدار / مبلغ واحد</label>
                <input type="number" id="servicePrice" min="0" />
              </div>
            </div>

            <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

            <div class="details-box" id="detailsBox">
              <div class="field">
                <label>توضیحات</label>
                <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
              </div>
            </div>

            <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
          </div>

          <div class="records-card">
            <div class="records-title">
              <strong>ثبت‌های امروز</strong>
              <span id="recordsCountText">۰ مورد</span>
            </div>

            <div id="recordsList">
              <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
            </div>
          </div>

        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  * {
    box-sizing: border-box;
  }

  .worker-page {
    max-width: 520px;
    margin: 0 auto;
  }

  .page-header {
    margin-bottom: 14px;
  }

  .page-title {
    font-size: 18px;
    font-weight: 900;
    margin: 0 0 5px;
    color: #111827;
  }

  .page-subtitle {
    font-size: 12px;
    color: #6b7280;
    margin: 0;
    line-height: 1.8;
  }

  .search-card,
  .form-card,
  .records-card {
    background: #ffffff;
    border-radius: 20px;
    padding: 13px;
    box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
    margin-bottom: 13px;
    border: 1px solid #e5e7eb;
  }

  .search-label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    margin-bottom: 8px;
    color: #374151;
  }

  .search-input-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
    background: #f9fafb;
    border: 2px solid #2563eb;
    border-radius: 15px;
    padding: 10px 12px;
  }

  .search-icon {
    font-size: 17px;
  }

  #serviceSearch {
    width: 100%;
    border: none;
    outline: none;
    background: transparent;
    font-size: 14px;
    font-weight: 700;
    color: #111827;
  }

  #serviceSearch::placeholder {
    color: #9ca3af;
    font-weight: 500;
  }

  .service-results {
    margin-top: 10px;
    display: none;
  }

  .service-result-item {
    background: #f8fafc;
    border: 1px solid #e5e7eb;
    border-radius: 13px;
    padding: 10px;
    margin-bottom: 7px;
    cursor: pointer;
  }

  .service-result-item:hover {
    background: #eef2ff;
    border-color: #c7d2fe;
  }

  .service-result-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 3px;
  }

  .service-result-price {
    font-size: 11px;
    color: #6b7280;
  }

  .summary-wrap {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 12px;
  }

  .summary-wrap .summary-card {
    background: #ffffff;
    color: #111827;
    border-radius: 17px;
    padding: 12px;
    box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
    border: 1px solid #e5e7eb;
  }

  .summary-wrap .summary-card span {
    display: block;
    color: #6b7280;
    font-size: 11px;
    font-weight: 700;
    margin-bottom: 6px;
  }

  .summary-wrap .summary-card strong {
    display: block;
    color: #111827;
    font-size: 15px;
    font-weight: 900;
  }

  #todayAmount {
    color: #16a34a;
  }

  .personal-record-card {
    background: linear-gradient(135deg, #fff7ed, #fffbeb);
    border: 1px solid #fed7aa;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    display: flex;
    align-items: center;
    gap: 11px;
    box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
  }

  .record-icon {
    width: 42px;
    height: 42px;
    border-radius: 14px;
    background: #ffedd5;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 21px;
    flex-shrink: 0;
  }

  .record-content {
    flex: 1;
  }

  .record-content span {
    display: block;
    color: #9a3412;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content strong {
    display: block;
    color: #111827;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content small {
    display: none;
    color: #92400e;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.7;
  }

  .feedback-card {
    background: linear-gradient(135deg, #eff6ff, #f8fafc);
    border: 1px solid #bfdbfe;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
  }

  .feedback-title {
    font-size: 12px;
    font-weight: 900;
    color: #1d4ed8;
    margin-bottom: 7px;
  }

  .feedback-main {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 5px;
  }

  .feedback-sub {
    font-size: 11px;
    line-height: 1.8;
    color: #4b5563;
  }

  .feedback-badge {
    display: inline-block;
    margin-top: 8px;
    padding: 5px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 900;
  }

  .feedback-badge.positive {
    background: #dbeafe;
    color: #1d4ed8;
  }

  .feedback-badge.negative {
    background: #fef3c7;
    color: #92400e;
  }

  .feedback-badge.neutral {
    background: #e5e7eb;
    color: #374151;
  }

  .form-card {
    display: none;
  }

  .selected-service {
    background: #eff6ff;
    border: 1px solid #bfdbfe;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 12px;
  }

  .selected-service span {
    display: block;
    color: #1d4ed8;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .selected-service strong {
    display: block;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .field {
    margin-bottom: 10px;
  }

  .field label {
    display: block;
    font-size: 11px;
    font-weight: 900;
    color: #374151;
    margin-bottom: 6px;
  }

  .field input,
  .field textarea {
    width: 100%;
    border: 1px solid #d1d5db;
    outline: none;
    background: #f9fafb;
    border-radius: 13px;
    padding: 10px;
    font-size: 13px;
    font-family: inherit;
  }

  .field input:focus,
  .field textarea:focus {
    border-color: #2563eb;
    background: #ffffff;
  }

  .field textarea {
    min-height: 75px;
    resize: vertical;
    line-height: 1.8;
  }

  .details-toggle {
    width: 100%;
    border: none;
    background: #f3f4f6;
    color: #374151;
    border-radius: 13px;
    padding: 10px;
    font-size: 12px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
    margin-bottom: 10px;
  }

  .details-box {
    display: none;
  }

  .submit-btn {
    width: 100%;
    border: none;
    background: #2563eb;
    color: #ffffff;
    border-radius: 15px;
    padding: 12px;
    font-size: 14px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
  }

  .records-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }

  .records-title strong {
    font-size: 14px;
    font-weight: 900;
    color: #111827;
  }

  .records-title span {
    font-size: 11px;
    color: #6b7280;
    font-weight: 700;
  }

  .empty-records {
    background: #f9fafb;
    color: #6b7280;
    text-align: center;
    border-radius: 14px;
    padding: 16px 10px;
    font-size: 12px;
    line-height: 1.8;
  }

  .record-item {
    border: 1px solid #e5e7eb;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 9px;
    background: #ffffff;
  }

  .record-item:last-child {
    margin-bottom: 0;
  }

  .record-top {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 7px;
  }

  .record-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
  }

  .record-time {
    font-size: 10px;
    color: #9ca3af;
    white-space: nowrap;
  }

  .record-info {
    font-size: 11px;
    color: #4b5563;
    line-height: 1.9;
  }

  .record-total {
    margin-top: 6px;
    font-size: 12px;
    font-weight: 900;
    color: #16a34a;
  }

  .record-desc {
    margin-top: 5px;
    color: #6b7280;
    font-size: 11px;
    line-height: 1.8;
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }

  @media (max-width: 380px) {
    .summary-wrap .summary-card strong {
      font-size: 14px;
    }
  }
</style>

<script>
(function(){
  let selectedService = null;
  let records = [];

  let latestFeedback = {
    type: "positive",
    title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
    description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
    badge: "۸۰٪"
  };

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const services = [
    { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
    { name: "جوشکاری", price: 200000 },
    { name: "نجاری", price: 180000 }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const toast = document.getElementById("toast");

  const serviceSearch = document.getElementById("serviceSearch");
  const serviceResults = document.getElementById("serviceResults");
  const serviceForm = document.getElementById("serviceForm");
  const selectedServiceName = document.getElementById("selectedServiceName");
  const serviceCount = document.getElementById("serviceCount");
  const servicePrice = document.getElementById("servicePrice");
  const serviceDescription = document.getElementById("serviceDescription");
  const submitService = document.getElementById("submitService");
  const todayAmount = document.getElementById("todayAmount");
  const todayCount = document.getElementById("todayCount");
  const recordsList = document.getElementById("recordsList");
  const recordsCountText = document.getElementById("recordsCountText");
  const detailsToggle = document.getElementById("detailsToggle");
  const detailsBox = document.getElementById("detailsBox");
  const bestRecordText = document.getElementById("bestRecordText");
  const recordMessage = document.getElementById("recordMessage");
  const feedbackMain = document.getElementById("feedbackMain");
  const feedbackSub = document.getElementById("feedbackSub");
  const feedbackBadge = document.getElementById("feedbackBadge");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function toPersianNumber(value) {
    return Number(value || 0).toLocaleString("fa-IR");
  }

  function formatToman(value) {
    return toPersianNumber(value) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function showResults(keyword) {
    const text = keyword.trim();
    serviceResults.innerHTML = "";

    if (!text) {
      serviceResults.style.display = "none";
      return;
    }

    const filtered = services.filter(function(service) {
      return service.name.includes(text);
    });

    if (filtered.length === 0) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
        <div class="service-result-price">برای انتخاب این مورد بزنید</div>
      `;
      item.addEventListener("click", function() {
        selectService({ name: text, price: 0 });
      });
      serviceResults.appendChild(item);
      serviceResults.style.display = "block";
      return;
    }

    filtered.forEach(function(service) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">${service.name}</div>
        <div class="service-result-price">${formatToman(service.price)}</div>
      `;
      item.addEventListener("click", function() {
        selectService(service);
      });
      serviceResults.appendChild(item);
    });

    serviceResults.style.display = "block";
  }

  function selectService(service) {
    selectedService = service;
    selectedServiceName.textContent = service.name;
    serviceSearch.value = service.name;
    servicePrice.value = service.price || "";
    serviceCount.value = 1;
    serviceDescription.value = "";
    serviceResults.style.display = "none";
    serviceForm.style.display = "block";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";

    setTimeout(function() {
      serviceCount.focus();
    }, 100);
  }

  function updateSummary() {
    const totalAmount = records.reduce(function(sum, item) {
      return sum + item.total;
    }, 0);

    const totalCount = records.reduce(function(sum, item) {
      return sum + item.count;
    }, 0);

    todayAmount.textContent = formatToman(totalAmount);
    todayCount.textContent = toPersianNumber(totalCount);
  }

  function updatePersonalRecord() {
    bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
    recordMessage.textContent = "";
  }

  function renderRecords() {
    recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

    if (records.length === 0) {
      recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
      return;
    }

    recordsList.innerHTML = "";

    const reversed = records.slice().reverse();
    reversed.forEach(function(item) {
      const div = document.createElement("div");
      div.className = "record-item";
      div.innerHTML = `
        <div class="record-top">
          <div class="record-name">${item.name}</div>
          <div class="record-time">${item.time}</div>
        </div>
        <div class="record-info">
          تعداد: ${toPersianNumber(item.count)} |
          مبلغ واحد: ${formatToman(item.price)}
        </div>
        <div class="record-total">
          جمع: ${formatToman(item.total)}
        </div>
        ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
      `;
      recordsList.appendChild(div);
    });
  }

  function renderFeedback() {
    feedbackMain.textContent = latestFeedback.title;
    feedbackSub.textContent = latestFeedback.description;
    feedbackBadge.textContent = latestFeedback.badge;
    feedbackBadge.className = "feedback-badge " + latestFeedback.type;
  }

  function submitRecord() {
    if (!selectedService) {
      alert("اول یک خدمت را انتخاب کن.");
      return;
    }

    const count = parseInt(serviceCount.value, 10);
    const price = parseInt(servicePrice.value, 10);
    const description = serviceDescription.value.trim();

    if (!count || count <= 0) {
      alert("تعداد را درست وارد کن.");
      return;
    }

    if (isNaN(price) || price < 0) {
      alert("مبلغ را درست وارد کن.");
      return;
    }

    const total = count * price;
    const now = new Date();

    records.push({
      name: selectedService.name,
      count: count,
      price: price,
      total: total,
      description: description,
      time: now.toLocaleTimeString("fa-IR", {
        hour: "2-digit",
        minute: "2-digit"
      })
    });

    renderRecords();
    updateSummary();

    selectedService = null;
    serviceSearch.value = "";
    serviceCount.value = 1;
    servicePrice.value = "";
    serviceDescription.value = "";
    selectedServiceName.textContent = "---";
    serviceForm.style.display = "none";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
    serviceSearch.focus();

    showToast("ثبت جدید اضافه شد");
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmountValue);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderRecords();
    updateSummary();
    updatePersonalRecord();
    renderFeedback();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function() {
    showResults(serviceSearch.value);
  });

  detailsToggle.addEventListener("click", function() {
    if (detailsBox.style.display === "block") {
      detailsBox.style.display = "none";
      detailsToggle.textContent = "افزودن توضیحات اختیاری";
    } else {
      detailsBox.style.display = "block";
      detailsToggle.textContent = "بستن توضیحات";
    }
  });

  submitService.addEventListener("click", submitRecord);

  renderAll();
})();
</script>
    <?php
    return ob_get_clean();
});
کد عالی اپ
TEXT - 2026-05-12 01:03:38
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage"></small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } * { box-sizing: border-box; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-wrap .summary-card { background: #ffffff; color: #111827; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-wrap .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-wrap .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: none; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } @media (max-width: 380px) { .summary-wrap .summary-card strong { font-size: 14px; } } </style> <script> (function(){ let selectedService = null; let records = []; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const toast = document.getElementById("toast"); const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); } function updatePersonalRecord() { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = ""; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); showToast("ثبت جدید اضافه شد"); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmountValue); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderRecords(); updateSummary(); updatePersonalRecord(); renderFeedback(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="worker-page">

          <div class="page-header">
            <h1 class="page-title">ثبت کار امروز</h1>
            <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
          </div>

          <div class="search-card">
            <label class="search-label">جستجوی خدمت</label>

            <div class="search-input-wrap">
              <div class="search-icon">🔍</div>
              <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
            </div>

            <div class="service-results" id="serviceResults"></div>
          </div>

          <div class="summary-wrap">
            <div class="summary-card">
              <span>مبلغ امروز</span>
              <strong id="todayAmount">۰ تومان</strong>
            </div>

            <div class="summary-card">
              <span>تعداد امروز</span>
              <strong id="todayCount">۰</strong>
            </div>
          </div>

          <div class="personal-record-card">
            <div class="record-icon">🏆</div>
            <div class="record-content">
              <span>رکورد روزانه تو</span>
              <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
              <small id="recordMessage"></small>
            </div>
          </div>

          <div class="feedback-card">
            <div class="feedback-title">آخرین بازخورد عملکرد</div>
            <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
            <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
            <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
          </div>

          <div class="form-card" id="serviceForm">
            <div class="selected-service">
              <span>خدمت انتخاب شده</span>
              <strong id="selectedServiceName">---</strong>
            </div>

            <div class="form-grid">
              <div class="field">
                <label>تعداد</label>
                <input type="number" id="serviceCount" min="1" value="1" />
              </div>

              <div class="field">
                <label>مقدار / مبلغ واحد</label>
                <input type="number" id="servicePrice" min="0" />
              </div>
            </div>

            <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

            <div class="details-box" id="detailsBox">
              <div class="field">
                <label>توضیحات</label>
                <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
              </div>
            </div>

            <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
          </div>

          <div class="records-card">
            <div class="records-title">
              <strong>ثبت‌های امروز</strong>
              <span id="recordsCountText">۰ مورد</span>
            </div>

            <div id="recordsList">
              <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
            </div>
          </div>

        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  * {
    box-sizing: border-box;
  }

  .worker-page {
    max-width: 520px;
    margin: 0 auto;
  }

  .page-header {
    margin-bottom: 14px;
  }

  .page-title {
    font-size: 18px;
    font-weight: 900;
    margin: 0 0 5px;
    color: #111827;
  }

  .page-subtitle {
    font-size: 12px;
    color: #6b7280;
    margin: 0;
    line-height: 1.8;
  }

  .search-card,
  .form-card,
  .records-card {
    background: #ffffff;
    border-radius: 20px;
    padding: 13px;
    box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
    margin-bottom: 13px;
    border: 1px solid #e5e7eb;
  }

  .search-label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    margin-bottom: 8px;
    color: #374151;
  }

  .search-input-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
    background: #f9fafb;
    border: 2px solid #2563eb;
    border-radius: 15px;
    padding: 10px 12px;
  }

  .search-icon {
    font-size: 17px;
  }

  #serviceSearch {
    width: 100%;
    border: none;
    outline: none;
    background: transparent;
    font-size: 14px;
    font-weight: 700;
    color: #111827;
  }

  #serviceSearch::placeholder {
    color: #9ca3af;
    font-weight: 500;
  }

  .service-results {
    margin-top: 10px;
    display: none;
  }

  .service-result-item {
    background: #f8fafc;
    border: 1px solid #e5e7eb;
    border-radius: 13px;
    padding: 10px;
    margin-bottom: 7px;
    cursor: pointer;
  }

  .service-result-item:hover {
    background: #eef2ff;
    border-color: #c7d2fe;
  }

  .service-result-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 3px;
  }

  .service-result-price {
    font-size: 11px;
    color: #6b7280;
  }

  .summary-wrap {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 12px;
  }

  .summary-wrap .summary-card {
    background: #ffffff;
    color: #111827;
    border-radius: 17px;
    padding: 12px;
    box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
    border: 1px solid #e5e7eb;
  }

  .summary-wrap .summary-card span {
    display: block;
    color: #6b7280;
    font-size: 11px;
    font-weight: 700;
    margin-bottom: 6px;
  }

  .summary-wrap .summary-card strong {
    display: block;
    color: #111827;
    font-size: 15px;
    font-weight: 900;
  }

  #todayAmount {
    color: #16a34a;
  }

  .personal-record-card {
    background: linear-gradient(135deg, #fff7ed, #fffbeb);
    border: 1px solid #fed7aa;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    display: flex;
    align-items: center;
    gap: 11px;
    box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
  }

  .record-icon {
    width: 42px;
    height: 42px;
    border-radius: 14px;
    background: #ffedd5;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 21px;
    flex-shrink: 0;
  }

  .record-content {
    flex: 1;
  }

  .record-content span {
    display: block;
    color: #9a3412;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content strong {
    display: block;
    color: #111827;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content small {
    display: none;
    color: #92400e;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.7;
  }

  .feedback-card {
    background: linear-gradient(135deg, #eff6ff, #f8fafc);
    border: 1px solid #bfdbfe;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
  }

  .feedback-title {
    font-size: 12px;
    font-weight: 900;
    color: #1d4ed8;
    margin-bottom: 7px;
  }

  .feedback-main {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 5px;
  }

  .feedback-sub {
    font-size: 11px;
    line-height: 1.8;
    color: #4b5563;
  }

  .feedback-badge {
    display: inline-block;
    margin-top: 8px;
    padding: 5px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 900;
  }

  .feedback-badge.positive {
    background: #dbeafe;
    color: #1d4ed8;
  }

  .feedback-badge.negative {
    background: #fef3c7;
    color: #92400e;
  }

  .feedback-badge.neutral {
    background: #e5e7eb;
    color: #374151;
  }

  .form-card {
    display: none;
  }

  .selected-service {
    background: #eff6ff;
    border: 1px solid #bfdbfe;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 12px;
  }

  .selected-service span {
    display: block;
    color: #1d4ed8;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .selected-service strong {
    display: block;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .field {
    margin-bottom: 10px;
  }

  .field label {
    display: block;
    font-size: 11px;
    font-weight: 900;
    color: #374151;
    margin-bottom: 6px;
  }

  .field input,
  .field textarea {
    width: 100%;
    border: 1px solid #d1d5db;
    outline: none;
    background: #f9fafb;
    border-radius: 13px;
    padding: 10px;
    font-size: 13px;
    font-family: inherit;
  }

  .field input:focus,
  .field textarea:focus {
    border-color: #2563eb;
    background: #ffffff;
  }

  .field textarea {
    min-height: 75px;
    resize: vertical;
    line-height: 1.8;
  }

  .details-toggle {
    width: 100%;
    border: none;
    background: #f3f4f6;
    color: #374151;
    border-radius: 13px;
    padding: 10px;
    font-size: 12px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
    margin-bottom: 10px;
  }

  .details-box {
    display: none;
  }

  .submit-btn {
    width: 100%;
    border: none;
    background: #2563eb;
    color: #ffffff;
    border-radius: 15px;
    padding: 12px;
    font-size: 14px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
  }

  .records-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }

  .records-title strong {
    font-size: 14px;
    font-weight: 900;
    color: #111827;
  }

  .records-title span {
    font-size: 11px;
    color: #6b7280;
    font-weight: 700;
  }

  .empty-records {
    background: #f9fafb;
    color: #6b7280;
    text-align: center;
    border-radius: 14px;
    padding: 16px 10px;
    font-size: 12px;
    line-height: 1.8;
  }

  .record-item {
    border: 1px solid #e5e7eb;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 9px;
    background: #ffffff;
  }

  .record-item:last-child {
    margin-bottom: 0;
  }

  .record-top {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 7px;
  }

  .record-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
  }

  .record-time {
    font-size: 10px;
    color: #9ca3af;
    white-space: nowrap;
  }

  .record-info {
    font-size: 11px;
    color: #4b5563;
    line-height: 1.9;
  }

  .record-total {
    margin-top: 6px;
    font-size: 12px;
    font-weight: 900;
    color: #16a34a;
  }

  .record-desc {
    margin-top: 5px;
    color: #6b7280;
    font-size: 11px;
    line-height: 1.8;
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }

  @media (max-width: 380px) {
    .summary-wrap .summary-card strong {
      font-size: 14px;
    }
  }
</style>

<script>
(function(){
  let selectedService = null;
  let records = [];

  let latestFeedback = {
    type: "positive",
    title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
    description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
    badge: "۸۰٪"
  };

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const services = [
    { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
    { name: "جوشکاری", price: 200000 },
    { name: "نجاری", price: 180000 }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const toast = document.getElementById("toast");

  const serviceSearch = document.getElementById("serviceSearch");
  const serviceResults = document.getElementById("serviceResults");
  const serviceForm = document.getElementById("serviceForm");
  const selectedServiceName = document.getElementById("selectedServiceName");
  const serviceCount = document.getElementById("serviceCount");
  const servicePrice = document.getElementById("servicePrice");
  const serviceDescription = document.getElementById("serviceDescription");
  const submitService = document.getElementById("submitService");
  const todayAmount = document.getElementById("todayAmount");
  const todayCount = document.getElementById("todayCount");
  const recordsList = document.getElementById("recordsList");
  const recordsCountText = document.getElementById("recordsCountText");
  const detailsToggle = document.getElementById("detailsToggle");
  const detailsBox = document.getElementById("detailsBox");
  const bestRecordText = document.getElementById("bestRecordText");
  const recordMessage = document.getElementById("recordMessage");
  const feedbackMain = document.getElementById("feedbackMain");
  const feedbackSub = document.getElementById("feedbackSub");
  const feedbackBadge = document.getElementById("feedbackBadge");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function toPersianNumber(value) {
    return Number(value || 0).toLocaleString("fa-IR");
  }

  function formatToman(value) {
    return toPersianNumber(value) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function showResults(keyword) {
    const text = keyword.trim();
    serviceResults.innerHTML = "";

    if (!text) {
      serviceResults.style.display = "none";
      return;
    }

    const filtered = services.filter(function(service) {
      return service.name.includes(text);
    });

    if (filtered.length === 0) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
        <div class="service-result-price">برای انتخاب این مورد بزنید</div>
      `;
      item.addEventListener("click", function() {
        selectService({ name: text, price: 0 });
      });
      serviceResults.appendChild(item);
      serviceResults.style.display = "block";
      return;
    }

    filtered.forEach(function(service) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">${service.name}</div>
        <div class="service-result-price">${formatToman(service.price)}</div>
      `;
      item.addEventListener("click", function() {
        selectService(service);
      });
      serviceResults.appendChild(item);
    });

    serviceResults.style.display = "block";
  }

  function selectService(service) {
    selectedService = service;
    selectedServiceName.textContent = service.name;
    serviceSearch.value = service.name;
    servicePrice.value = service.price || "";
    serviceCount.value = 1;
    serviceDescription.value = "";
    serviceResults.style.display = "none";
    serviceForm.style.display = "block";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";

    setTimeout(function() {
      serviceCount.focus();
    }, 100);
  }

  function updateSummary() {
    const totalAmount = records.reduce(function(sum, item) {
      return sum + item.total;
    }, 0);

    const totalCount = records.reduce(function(sum, item) {
      return sum + item.count;
    }, 0);

    todayAmount.textContent = formatToman(totalAmount);
    todayCount.textContent = toPersianNumber(totalCount);
  }

  function updatePersonalRecord() {
    bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
    recordMessage.textContent = "";
  }

  function renderRecords() {
    recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

    if (records.length === 0) {
      recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
      return;
    }

    recordsList.innerHTML = "";

    const reversed = records.slice().reverse();
    reversed.forEach(function(item) {
      const div = document.createElement("div");
      div.className = "record-item";
      div.innerHTML = `
        <div class="record-top">
          <div class="record-name">${item.name}</div>
          <div class="record-time">${item.time}</div>
        </div>
        <div class="record-info">
          تعداد: ${toPersianNumber(item.count)} |
          مبلغ واحد: ${formatToman(item.price)}
        </div>
        <div class="record-total">
          جمع: ${formatToman(item.total)}
        </div>
        ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
      `;
      recordsList.appendChild(div);
    });
  }

  function renderFeedback() {
    feedbackMain.textContent = latestFeedback.title;
    feedbackSub.textContent = latestFeedback.description;
    feedbackBadge.textContent = latestFeedback.badge;
    feedbackBadge.className = "feedback-badge " + latestFeedback.type;
  }

  function submitRecord() {
    if (!selectedService) {
      alert("اول یک خدمت را انتخاب کن.");
      return;
    }

    const count = parseInt(serviceCount.value, 10);
    const price = parseInt(servicePrice.value, 10);
    const description = serviceDescription.value.trim();

    if (!count || count <= 0) {
      alert("تعداد را درست وارد کن.");
      return;
    }

    if (isNaN(price) || price < 0) {
      alert("مبلغ را درست وارد کن.");
      return;
    }

    const total = count * price;
    const now = new Date();

    records.push({
      name: selectedService.name,
      count: count,
      price: price,
      total: total,
      description: description,
      time: now.toLocaleTimeString("fa-IR", {
        hour: "2-digit",
        minute: "2-digit"
      })
    });

    renderRecords();
    updateSummary();

    selectedService = null;
    serviceSearch.value = "";
    serviceCount.value = 1;
    servicePrice.value = "";
    serviceDescription.value = "";
    selectedServiceName.textContent = "---";
    serviceForm.style.display = "none";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
    serviceSearch.focus();

    showToast("ثبت جدید اضافه شد");
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmountValue);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderRecords();
    updateSummary();
    updatePersonalRecord();
    renderFeedback();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function() {
    showResults(serviceSearch.value);
  });

  detailsToggle.addEventListener("click", function() {
    if (detailsBox.style.display === "block") {
      detailsBox.style.display = "none";
      detailsToggle.textContent = "افزودن توضیحات اختیاری";
    } else {
      detailsBox.style.display = "block";
      detailsToggle.textContent = "بستن توضیحات";
    }
  });

  submitService.addEventListener("click", submitRecord);

  renderAll();
})();
</script>
999
TEXT - 2026-05-12 00:41:12
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span> </div> </div> <div class="summary-grid"> <div class="summary-card dark"> <small>تعداد کل امروز</small> <strong id="regTotalQty">۰</strong> </div> <div class="summary-card green"> <small>جمع مبلغ امروز</small> <strong id="regTotalPrice">۰ تومان</strong> </div> </div> <div class="search-box"> <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..."> </div> <div class="section-title">خدمات پرکاربرد</div> <div class="chips" id="serviceChips"></div> <div id="selectedServiceBox" class="selected-box"> <div class="empty-box">یک خدمت را انتخاب کن</div> </div> <div class="section-title">ثبت‌های امروز</div> <div class="list-box" id="todayItems"> <div class="empty-list">هنوز چیزی ثبت نشده</div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } </style> <script> (function(){ const services = [ { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 }, { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 }, { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 }, { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 }, { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 }, { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 } ]; let selectedService = null; let currentQty = 1; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const chipsBox = document.getElementById("serviceChips"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const todayItems = document.getElementById("todayItems"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const serviceSearch = document.getElementById("serviceSearch"); const toast = document.getElementById("toast"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function getFilteredServices(){ const q = normalizeText(serviceSearch.value.trim()); if(!q) return services; return services.filter(s => normalizeText(s.name).includes(q)); } function renderChips(list = services){ chipsBox.innerHTML = ""; list.forEach(service => { const btn = document.createElement("button"); btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : ""); btn.textContent = service.name; btn.onclick = function(){ selectedService = service; currentQty = 1; renderChips(getFilteredServices()); renderSelectedService(); }; chipsBox.appendChild(btn); }); } function renderSelectedService(){ if(!selectedService){ selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`; return; } selectedServiceBox.innerHTML = ` <div class="service-card"> <div class="service-card-top"> <div> <h3>${selectedService.name}</h3> <p>قیمت واحد: ${money(selectedService.price)}</p> </div> <div class="price-badge">${money(selectedService.price * currentQty)}</div> </div> <div class="counter"> <button type="button" id="minusQty">−</button> <input type="number" id="qtyInput" min="1" value="${currentQty}"> <button type="button" id="plusQty">+</button> </div> <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button> </div> `; document.getElementById("minusQty").onclick = function(){ currentQty = Math.max(1, currentQty - 1); renderSelectedService(); }; document.getElementById("plusQty").onclick = function(){ currentQty++; renderSelectedService(); }; document.getElementById("qtyInput").oninput = function(e){ currentQty = Math.max(1, parseInt(e.target.value || "1")); renderSelectedService(); }; document.getElementById("addTodayBtn").onclick = function(){ entries.unshift({ id: Date.now(), serviceId: selectedService.id, name: selectedService.name, price: selectedService.price, qty: currentQty, status: "pending", worker: "عرفان", date: todayStr }); currentQty = 1; renderAll(); showToast("ثبت جدید اضافه شد"); }; } function renderTodayItems(){ const todayEntries = entries.filter(item => item.date === todayStr); if(todayEntries.length === 0){ todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`; return; } todayItems.innerHTML = ""; todayEntries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> `; row.querySelector(".btn-remove").onclick = function(){ entries = entries.filter(e => e.id !== item.id); renderAll(); showToast("آیتم حذف شد"); }; todayItems.appendChild(row); }); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderRegisterSummary(){ const todayEntries = entries.filter(item => item.date === todayStr); const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0); const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("regTotalQty").textContent = toFa(totalQty); document.getElementById("regTotalPrice").textContent = money(totalPrice); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmount = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmount); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderChips(getFilteredServices()); renderSelectedService(); renderTodayItems(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderRegisterSummary(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function(){ renderChips(getFilteredServices()); }); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
          </div>
        </div>

        <div class="summary-grid">
          <div class="summary-card dark">
            <small>تعداد کل امروز</small>
            <strong id="regTotalQty">۰</strong>
          </div>
          <div class="summary-card green">
            <small>جمع مبلغ امروز</small>
            <strong id="regTotalPrice">۰ تومان</strong>
          </div>
        </div>

        <div class="search-box">
          <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت...">
        </div>

        <div class="section-title">خدمات پرکاربرد</div>
        <div class="chips" id="serviceChips"></div>

        <div id="selectedServiceBox" class="selected-box">
          <div class="empty-box">یک خدمت را انتخاب کن</div>
        </div>

        <div class="section-title">ثبت‌های امروز</div>
        <div class="list-box" id="todayItems">
          <div class="empty-list">هنوز چیزی ثبت نشده</div>
        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }
</style>

<script>
(function(){
  const services = [
    { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 },
    { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 },
    { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 },
    { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 },
    { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 },
    { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 }
  ];

  let selectedService = null;
  let currentQty = 1;

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const chipsBox = document.getElementById("serviceChips");
  const selectedServiceBox = document.getElementById("selectedServiceBox");
  const todayItems = document.getElementById("todayItems");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const serviceSearch = document.getElementById("serviceSearch");
  const toast = document.getElementById("toast");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function getFilteredServices(){
    const q = normalizeText(serviceSearch.value.trim());
    if(!q) return services;
    return services.filter(s => normalizeText(s.name).includes(q));
  }

  function renderChips(list = services){
    chipsBox.innerHTML = "";
    list.forEach(service => {
      const btn = document.createElement("button");
      btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : "");
      btn.textContent = service.name;
      btn.onclick = function(){
        selectedService = service;
        currentQty = 1;
        renderChips(getFilteredServices());
        renderSelectedService();
      };
      chipsBox.appendChild(btn);
    });
  }

  function renderSelectedService(){
    if(!selectedService){
      selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`;
      return;
    }

    selectedServiceBox.innerHTML = `
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>${selectedService.name}</h3>
            <p>قیمت واحد: ${money(selectedService.price)}</p>
          </div>
          <div class="price-badge">${money(selectedService.price * currentQty)}</div>
        </div>

        <div class="counter">
          <button type="button" id="minusQty">−</button>
          <input type="number" id="qtyInput" min="1" value="${currentQty}">
          <button type="button" id="plusQty">+</button>
        </div>

        <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button>
      </div>
    `;

    document.getElementById("minusQty").onclick = function(){
      currentQty = Math.max(1, currentQty - 1);
      renderSelectedService();
    };

    document.getElementById("plusQty").onclick = function(){
      currentQty++;
      renderSelectedService();
    };

    document.getElementById("qtyInput").oninput = function(e){
      currentQty = Math.max(1, parseInt(e.target.value || "1"));
      renderSelectedService();
    };

    document.getElementById("addTodayBtn").onclick = function(){
      entries.unshift({
        id: Date.now(),
        serviceId: selectedService.id,
        name: selectedService.name,
        price: selectedService.price,
        qty: currentQty,
        status: "pending",
        worker: "عرفان",
        date: todayStr
      });
      currentQty = 1;
      renderAll();
      showToast("ثبت جدید اضافه شد");
    };
  }

  function renderTodayItems(){
    const todayEntries = entries.filter(item => item.date === todayStr);

    if(todayEntries.length === 0){
      todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`;
      return;
    }

    todayItems.innerHTML = "";
    todayEntries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      `;
      row.querySelector(".btn-remove").onclick = function(){
        entries = entries.filter(e => e.id !== item.id);
        renderAll();
        showToast("آیتم حذف شد");
      };
      todayItems.appendChild(row);
    });
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderRegisterSummary(){
    const todayEntries = entries.filter(item => item.date === todayStr);
    const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0);
    const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("regTotalQty").textContent = toFa(totalQty);
    document.getElementById("regTotalPrice").textContent = money(totalPrice);
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmount = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmount);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderChips(getFilteredServices());
    renderSelectedService();
    renderTodayItems();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderRegisterSummary();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function(){
    renderChips(getFilteredServices());
  });

  renderAll();
})();
</script>
۱۱۱۱۲
TEXT - 2026-05-12 00:41:01
add_shortcode('factory_app', function () { ob_start(); ?> <div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage"></small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } * { box-sizing: border-box; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-wrap .summary-card { background: #ffffff; color: #111827; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-wrap .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-wrap .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: none; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } @media (max-width: 380px) { .summary-wrap .summary-card strong { font-size: 14px; } } </style> <script> (function(){ let selectedService = null; let records = []; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const toast = document.getElementById("toast"); const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); } function updatePersonalRecord() { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = ""; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); showToast("ثبت جدید اضافه شد"); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmountValue); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderRecords(); updateSummary(); updatePersonalRecord(); renderFeedback(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); renderAll(); })(); </script> <?php return ob_get_clean(); });
add_shortcode('factory_app', function () {
    ob_start();
    ?>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="worker-page">

          <div class="page-header">
            <h1 class="page-title">ثبت کار امروز</h1>
            <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
          </div>

          <div class="search-card">
            <label class="search-label">جستجوی خدمت</label>

            <div class="search-input-wrap">
              <div class="search-icon">🔍</div>
              <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
            </div>

            <div class="service-results" id="serviceResults"></div>
          </div>

          <div class="summary-wrap">
            <div class="summary-card">
              <span>مبلغ امروز</span>
              <strong id="todayAmount">۰ تومان</strong>
            </div>

            <div class="summary-card">
              <span>تعداد امروز</span>
              <strong id="todayCount">۰</strong>
            </div>
          </div>

          <div class="personal-record-card">
            <div class="record-icon">🏆</div>
            <div class="record-content">
              <span>رکورد روزانه تو</span>
              <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
              <small id="recordMessage"></small>
            </div>
          </div>

          <div class="feedback-card">
            <div class="feedback-title">آخرین بازخورد عملکرد</div>
            <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
            <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
            <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
          </div>

          <div class="form-card" id="serviceForm">
            <div class="selected-service">
              <span>خدمت انتخاب شده</span>
              <strong id="selectedServiceName">---</strong>
            </div>

            <div class="form-grid">
              <div class="field">
                <label>تعداد</label>
                <input type="number" id="serviceCount" min="1" value="1" />
              </div>

              <div class="field">
                <label>مقدار / مبلغ واحد</label>
                <input type="number" id="servicePrice" min="0" />
              </div>
            </div>

            <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

            <div class="details-box" id="detailsBox">
              <div class="field">
                <label>توضیحات</label>
                <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
              </div>
            </div>

            <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
          </div>

          <div class="records-card">
            <div class="records-title">
              <strong>ثبت‌های امروز</strong>
              <span id="recordsCountText">۰ مورد</span>
            </div>

            <div id="recordsList">
              <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
            </div>
          </div>

        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  * {
    box-sizing: border-box;
  }

  .worker-page {
    max-width: 520px;
    margin: 0 auto;
  }

  .page-header {
    margin-bottom: 14px;
  }

  .page-title {
    font-size: 18px;
    font-weight: 900;
    margin: 0 0 5px;
    color: #111827;
  }

  .page-subtitle {
    font-size: 12px;
    color: #6b7280;
    margin: 0;
    line-height: 1.8;
  }

  .search-card,
  .form-card,
  .records-card {
    background: #ffffff;
    border-radius: 20px;
    padding: 13px;
    box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
    margin-bottom: 13px;
    border: 1px solid #e5e7eb;
  }

  .search-label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    margin-bottom: 8px;
    color: #374151;
  }

  .search-input-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
    background: #f9fafb;
    border: 2px solid #2563eb;
    border-radius: 15px;
    padding: 10px 12px;
  }

  .search-icon {
    font-size: 17px;
  }

  #serviceSearch {
    width: 100%;
    border: none;
    outline: none;
    background: transparent;
    font-size: 14px;
    font-weight: 700;
    color: #111827;
  }

  #serviceSearch::placeholder {
    color: #9ca3af;
    font-weight: 500;
  }

  .service-results {
    margin-top: 10px;
    display: none;
  }

  .service-result-item {
    background: #f8fafc;
    border: 1px solid #e5e7eb;
    border-radius: 13px;
    padding: 10px;
    margin-bottom: 7px;
    cursor: pointer;
  }

  .service-result-item:hover {
    background: #eef2ff;
    border-color: #c7d2fe;
  }

  .service-result-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 3px;
  }

  .service-result-price {
    font-size: 11px;
    color: #6b7280;
  }

  .summary-wrap {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 12px;
  }

  .summary-wrap .summary-card {
    background: #ffffff;
    color: #111827;
    border-radius: 17px;
    padding: 12px;
    box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
    border: 1px solid #e5e7eb;
  }

  .summary-wrap .summary-card span {
    display: block;
    color: #6b7280;
    font-size: 11px;
    font-weight: 700;
    margin-bottom: 6px;
  }

  .summary-wrap .summary-card strong {
    display: block;
    color: #111827;
    font-size: 15px;
    font-weight: 900;
  }

  #todayAmount {
    color: #16a34a;
  }

  .personal-record-card {
    background: linear-gradient(135deg, #fff7ed, #fffbeb);
    border: 1px solid #fed7aa;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    display: flex;
    align-items: center;
    gap: 11px;
    box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
  }

  .record-icon {
    width: 42px;
    height: 42px;
    border-radius: 14px;
    background: #ffedd5;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 21px;
    flex-shrink: 0;
  }

  .record-content {
    flex: 1;
  }

  .record-content span {
    display: block;
    color: #9a3412;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content strong {
    display: block;
    color: #111827;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content small {
    display: none;
    color: #92400e;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.7;
  }

  .feedback-card {
    background: linear-gradient(135deg, #eff6ff, #f8fafc);
    border: 1px solid #bfdbfe;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
  }

  .feedback-title {
    font-size: 12px;
    font-weight: 900;
    color: #1d4ed8;
    margin-bottom: 7px;
  }

  .feedback-main {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 5px;
  }

  .feedback-sub {
    font-size: 11px;
    line-height: 1.8;
    color: #4b5563;
  }

  .feedback-badge {
    display: inline-block;
    margin-top: 8px;
    padding: 5px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 900;
  }

  .feedback-badge.positive {
    background: #dbeafe;
    color: #1d4ed8;
  }

  .feedback-badge.negative {
    background: #fef3c7;
    color: #92400e;
  }

  .feedback-badge.neutral {
    background: #e5e7eb;
    color: #374151;
  }

  .form-card {
    display: none;
  }

  .selected-service {
    background: #eff6ff;
    border: 1px solid #bfdbfe;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 12px;
  }

  .selected-service span {
    display: block;
    color: #1d4ed8;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .selected-service strong {
    display: block;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .field {
    margin-bottom: 10px;
  }

  .field label {
    display: block;
    font-size: 11px;
    font-weight: 900;
    color: #374151;
    margin-bottom: 6px;
  }

  .field input,
  .field textarea {
    width: 100%;
    border: 1px solid #d1d5db;
    outline: none;
    background: #f9fafb;
    border-radius: 13px;
    padding: 10px;
    font-size: 13px;
    font-family: inherit;
  }

  .field input:focus,
  .field textarea:focus {
    border-color: #2563eb;
    background: #ffffff;
  }

  .field textarea {
    min-height: 75px;
    resize: vertical;
    line-height: 1.8;
  }

  .details-toggle {
    width: 100%;
    border: none;
    background: #f3f4f6;
    color: #374151;
    border-radius: 13px;
    padding: 10px;
    font-size: 12px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
    margin-bottom: 10px;
  }

  .details-box {
    display: none;
  }

  .submit-btn {
    width: 100%;
    border: none;
    background: #2563eb;
    color: #ffffff;
    border-radius: 15px;
    padding: 12px;
    font-size: 14px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
  }

  .records-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }

  .records-title strong {
    font-size: 14px;
    font-weight: 900;
    color: #111827;
  }

  .records-title span {
    font-size: 11px;
    color: #6b7280;
    font-weight: 700;
  }

  .empty-records {
    background: #f9fafb;
    color: #6b7280;
    text-align: center;
    border-radius: 14px;
    padding: 16px 10px;
    font-size: 12px;
    line-height: 1.8;
  }

  .record-item {
    border: 1px solid #e5e7eb;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 9px;
    background: #ffffff;
  }

  .record-item:last-child {
    margin-bottom: 0;
  }

  .record-top {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 7px;
  }

  .record-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
  }

  .record-time {
    font-size: 10px;
    color: #9ca3af;
    white-space: nowrap;
  }

  .record-info {
    font-size: 11px;
    color: #4b5563;
    line-height: 1.9;
  }

  .record-total {
    margin-top: 6px;
    font-size: 12px;
    font-weight: 900;
    color: #16a34a;
  }

  .record-desc {
    margin-top: 5px;
    color: #6b7280;
    font-size: 11px;
    line-height: 1.8;
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }

  @media (max-width: 380px) {
    .summary-wrap .summary-card strong {
      font-size: 14px;
    }
  }
</style>

<script>
(function(){
  let selectedService = null;
  let records = [];

  let latestFeedback = {
    type: "positive",
    title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
    description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
    badge: "۸۰٪"
  };

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const services = [
    { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
    { name: "جوشکاری", price: 200000 },
    { name: "نجاری", price: 180000 }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const toast = document.getElementById("toast");

  const serviceSearch = document.getElementById("serviceSearch");
  const serviceResults = document.getElementById("serviceResults");
  const serviceForm = document.getElementById("serviceForm");
  const selectedServiceName = document.getElementById("selectedServiceName");
  const serviceCount = document.getElementById("serviceCount");
  const servicePrice = document.getElementById("servicePrice");
  const serviceDescription = document.getElementById("serviceDescription");
  const submitService = document.getElementById("submitService");
  const todayAmount = document.getElementById("todayAmount");
  const todayCount = document.getElementById("todayCount");
  const recordsList = document.getElementById("recordsList");
  const recordsCountText = document.getElementById("recordsCountText");
  const detailsToggle = document.getElementById("detailsToggle");
  const detailsBox = document.getElementById("detailsBox");
  const bestRecordText = document.getElementById("bestRecordText");
  const recordMessage = document.getElementById("recordMessage");
  const feedbackMain = document.getElementById("feedbackMain");
  const feedbackSub = document.getElementById("feedbackSub");
  const feedbackBadge = document.getElementById("feedbackBadge");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function toPersianNumber(value) {
    return Number(value || 0).toLocaleString("fa-IR");
  }

  function formatToman(value) {
    return toPersianNumber(value) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function showResults(keyword) {
    const text = keyword.trim();
    serviceResults.innerHTML = "";

    if (!text) {
      serviceResults.style.display = "none";
      return;
    }

    const filtered = services.filter(function(service) {
      return service.name.includes(text);
    });

    if (filtered.length === 0) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
        <div class="service-result-price">برای انتخاب این مورد بزنید</div>
      `;
      item.addEventListener("click", function() {
        selectService({ name: text, price: 0 });
      });
      serviceResults.appendChild(item);
      serviceResults.style.display = "block";
      return;
    }

    filtered.forEach(function(service) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">${service.name}</div>
        <div class="service-result-price">${formatToman(service.price)}</div>
      `;
      item.addEventListener("click", function() {
        selectService(service);
      });
      serviceResults.appendChild(item);
    });

    serviceResults.style.display = "block";
  }

  function selectService(service) {
    selectedService = service;
    selectedServiceName.textContent = service.name;
    serviceSearch.value = service.name;
    servicePrice.value = service.price || "";
    serviceCount.value = 1;
    serviceDescription.value = "";
    serviceResults.style.display = "none";
    serviceForm.style.display = "block";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";

    setTimeout(function() {
      serviceCount.focus();
    }, 100);
  }

  function updateSummary() {
    const totalAmount = records.reduce(function(sum, item) {
      return sum + item.total;
    }, 0);

    const totalCount = records.reduce(function(sum, item) {
      return sum + item.count;
    }, 0);

    todayAmount.textContent = formatToman(totalAmount);
    todayCount.textContent = toPersianNumber(totalCount);
  }

  function updatePersonalRecord() {
    bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
    recordMessage.textContent = "";
  }

  function renderRecords() {
    recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

    if (records.length === 0) {
      recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
      return;
    }

    recordsList.innerHTML = "";

    const reversed = records.slice().reverse();
    reversed.forEach(function(item) {
      const div = document.createElement("div");
      div.className = "record-item";
      div.innerHTML = `
        <div class="record-top">
          <div class="record-name">${item.name}</div>
          <div class="record-time">${item.time}</div>
        </div>
        <div class="record-info">
          تعداد: ${toPersianNumber(item.count)} |
          مبلغ واحد: ${formatToman(item.price)}
        </div>
        <div class="record-total">
          جمع: ${formatToman(item.total)}
        </div>
        ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
      `;
      recordsList.appendChild(div);
    });
  }

  function renderFeedback() {
    feedbackMain.textContent = latestFeedback.title;
    feedbackSub.textContent = latestFeedback.description;
    feedbackBadge.textContent = latestFeedback.badge;
    feedbackBadge.className = "feedback-badge " + latestFeedback.type;
  }

  function submitRecord() {
    if (!selectedService) {
      alert("اول یک خدمت را انتخاب کن.");
      return;
    }

    const count = parseInt(serviceCount.value, 10);
    const price = parseInt(servicePrice.value, 10);
    const description = serviceDescription.value.trim();

    if (!count || count <= 0) {
      alert("تعداد را درست وارد کن.");
      return;
    }

    if (isNaN(price) || price < 0) {
      alert("مبلغ را درست وارد کن.");
      return;
    }

    const total = count * price;
    const now = new Date();

    records.push({
      name: selectedService.name,
      count: count,
      price: price,
      total: total,
      description: description,
      time: now.toLocaleTimeString("fa-IR", {
        hour: "2-digit",
        minute: "2-digit"
      })
    });

    renderRecords();
    updateSummary();

    selectedService = null;
    serviceSearch.value = "";
    serviceCount.value = 1;
    servicePrice.value = "";
    serviceDescription.value = "";
    selectedServiceName.textContent = "---";
    serviceForm.style.display = "none";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
    serviceSearch.focus();

    showToast("ثبت جدید اضافه شد");
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmountValue);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderRecords();
    updateSummary();
    updatePersonalRecord();
    renderFeedback();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function() {
    showResults(serviceSearch.value);
  });

  detailsToggle.addEventListener("click", function() {
    if (detailsBox.style.display === "block") {
      detailsBox.style.display = "none";
      detailsToggle.textContent = "افزودن توضیحات اختیاری";
    } else {
      detailsBox.style.display = "block";
      detailsToggle.textContent = "بستن توضیحات";
    }
  });

  submitService.addEventListener("click", submitRecord);

  renderAll();
})();
</script>
    <?php
    return ob_get_clean();
});
999
TEXT - 2026-05-12 00:34:31
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span> </div> </div> <div class="summary-grid"> <div class="summary-card dark"> <small>تعداد کل امروز</small> <strong id="regTotalQty">۰</strong> </div> <div class="summary-card green"> <small>جمع مبلغ امروز</small> <strong id="regTotalPrice">۰ تومان</strong> </div> </div> <div class="search-box"> <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..."> </div> <div class="section-title">خدمات پرکاربرد</div> <div class="chips" id="serviceChips"></div> <div id="selectedServiceBox" class="selected-box"> <div class="empty-box">یک خدمت را انتخاب کن</div> </div> <div class="section-title">ثبت‌های امروز</div> <div class="list-box" id="todayItems"> <div class="empty-list">هنوز چیزی ثبت نشده</div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } </style> <script> (function(){ const services = [ { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 }, { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 }, { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 }, { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 }, { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 }, { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 } ]; let selectedService = null; let currentQty = 1; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const chipsBox = document.getElementById("serviceChips"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const todayItems = document.getElementById("todayItems"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const serviceSearch = document.getElementById("serviceSearch"); const toast = document.getElementById("toast"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function getFilteredServices(){ const q = normalizeText(serviceSearch.value.trim()); if(!q) return services; return services.filter(s => normalizeText(s.name).includes(q)); } function renderChips(list = services){ chipsBox.innerHTML = ""; list.forEach(service => { const btn = document.createElement("button"); btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : ""); btn.textContent = service.name; btn.onclick = function(){ selectedService = service; currentQty = 1; renderChips(getFilteredServices()); renderSelectedService(); }; chipsBox.appendChild(btn); }); } function renderSelectedService(){ if(!selectedService){ selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`; return; } selectedServiceBox.innerHTML = ` <div class="service-card"> <div class="service-card-top"> <div> <h3>${selectedService.name}</h3> <p>قیمت واحد: ${money(selectedService.price)}</p> </div> <div class="price-badge">${money(selectedService.price * currentQty)}</div> </div> <div class="counter"> <button type="button" id="minusQty">−</button> <input type="number" id="qtyInput" min="1" value="${currentQty}"> <button type="button" id="plusQty">+</button> </div> <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button> </div> `; document.getElementById("minusQty").onclick = function(){ currentQty = Math.max(1, currentQty - 1); renderSelectedService(); }; document.getElementById("plusQty").onclick = function(){ currentQty++; renderSelectedService(); }; document.getElementById("qtyInput").oninput = function(e){ currentQty = Math.max(1, parseInt(e.target.value || "1")); renderSelectedService(); }; document.getElementById("addTodayBtn").onclick = function(){ entries.unshift({ id: Date.now(), serviceId: selectedService.id, name: selectedService.name, price: selectedService.price, qty: currentQty, status: "pending", worker: "عرفان", date: todayStr }); currentQty = 1; renderAll(); showToast("ثبت جدید اضافه شد"); }; } function renderTodayItems(){ const todayEntries = entries.filter(item => item.date === todayStr); if(todayEntries.length === 0){ todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`; return; } todayItems.innerHTML = ""; todayEntries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> `; row.querySelector(".btn-remove").onclick = function(){ entries = entries.filter(e => e.id !== item.id); renderAll(); showToast("آیتم حذف شد"); }; todayItems.appendChild(row); }); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderRegisterSummary(){ const todayEntries = entries.filter(item => item.date === todayStr); const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0); const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("regTotalQty").textContent = toFa(totalQty); document.getElementById("regTotalPrice").textContent = money(totalPrice); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmount = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmount); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderChips(getFilteredServices()); renderSelectedService(); renderTodayItems(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderRegisterSummary(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function(){ renderChips(getFilteredServices()); }); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
          </div>
        </div>

        <div class="summary-grid">
          <div class="summary-card dark">
            <small>تعداد کل امروز</small>
            <strong id="regTotalQty">۰</strong>
          </div>
          <div class="summary-card green">
            <small>جمع مبلغ امروز</small>
            <strong id="regTotalPrice">۰ تومان</strong>
          </div>
        </div>

        <div class="search-box">
          <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت...">
        </div>

        <div class="section-title">خدمات پرکاربرد</div>
        <div class="chips" id="serviceChips"></div>

        <div id="selectedServiceBox" class="selected-box">
          <div class="empty-box">یک خدمت را انتخاب کن</div>
        </div>

        <div class="section-title">ثبت‌های امروز</div>
        <div class="list-box" id="todayItems">
          <div class="empty-list">هنوز چیزی ثبت نشده</div>
        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }
</style>

<script>
(function(){
  const services = [
    { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 },
    { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 },
    { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 },
    { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 },
    { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 },
    { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 }
  ];

  let selectedService = null;
  let currentQty = 1;

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const chipsBox = document.getElementById("serviceChips");
  const selectedServiceBox = document.getElementById("selectedServiceBox");
  const todayItems = document.getElementById("todayItems");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const serviceSearch = document.getElementById("serviceSearch");
  const toast = document.getElementById("toast");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function getFilteredServices(){
    const q = normalizeText(serviceSearch.value.trim());
    if(!q) return services;
    return services.filter(s => normalizeText(s.name).includes(q));
  }

  function renderChips(list = services){
    chipsBox.innerHTML = "";
    list.forEach(service => {
      const btn = document.createElement("button");
      btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : "");
      btn.textContent = service.name;
      btn.onclick = function(){
        selectedService = service;
        currentQty = 1;
        renderChips(getFilteredServices());
        renderSelectedService();
      };
      chipsBox.appendChild(btn);
    });
  }

  function renderSelectedService(){
    if(!selectedService){
      selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`;
      return;
    }

    selectedServiceBox.innerHTML = `
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>${selectedService.name}</h3>
            <p>قیمت واحد: ${money(selectedService.price)}</p>
          </div>
          <div class="price-badge">${money(selectedService.price * currentQty)}</div>
        </div>

        <div class="counter">
          <button type="button" id="minusQty">−</button>
          <input type="number" id="qtyInput" min="1" value="${currentQty}">
          <button type="button" id="plusQty">+</button>
        </div>

        <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button>
      </div>
    `;

    document.getElementById("minusQty").onclick = function(){
      currentQty = Math.max(1, currentQty - 1);
      renderSelectedService();
    };

    document.getElementById("plusQty").onclick = function(){
      currentQty++;
      renderSelectedService();
    };

    document.getElementById("qtyInput").oninput = function(e){
      currentQty = Math.max(1, parseInt(e.target.value || "1"));
      renderSelectedService();
    };

    document.getElementById("addTodayBtn").onclick = function(){
      entries.unshift({
        id: Date.now(),
        serviceId: selectedService.id,
        name: selectedService.name,
        price: selectedService.price,
        qty: currentQty,
        status: "pending",
        worker: "عرفان",
        date: todayStr
      });
      currentQty = 1;
      renderAll();
      showToast("ثبت جدید اضافه شد");
    };
  }

  function renderTodayItems(){
    const todayEntries = entries.filter(item => item.date === todayStr);

    if(todayEntries.length === 0){
      todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`;
      return;
    }

    todayItems.innerHTML = "";
    todayEntries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      `;
      row.querySelector(".btn-remove").onclick = function(){
        entries = entries.filter(e => e.id !== item.id);
        renderAll();
        showToast("آیتم حذف شد");
      };
      todayItems.appendChild(row);
    });
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderRegisterSummary(){
    const todayEntries = entries.filter(item => item.date === todayStr);
    const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0);
    const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("regTotalQty").textContent = toFa(totalQty);
    document.getElementById("regTotalPrice").textContent = money(totalPrice);
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmount = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmount);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderChips(getFilteredServices());
    renderSelectedService();
    renderTodayItems();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderRegisterSummary();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function(){
    renderChips(getFilteredServices());
  });

  renderAll();
})();
</script>
۱۰۱۰۱۰
TEXT - 2026-05-12 00:34:18
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage"></small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } * { box-sizing: border-box; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-wrap .summary-card { background: #ffffff; color: #111827; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-wrap .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-wrap .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: none; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } @media (max-width: 380px) { .summary-wrap .summary-card strong { font-size: 14px; } } </style> <script> (function(){ let selectedService = null; let records = []; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const toast = document.getElementById("toast"); const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); } function updatePersonalRecord() { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = ""; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); showToast("ثبت جدید اضافه شد"); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmountValue); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderRecords(); updateSummary(); updatePersonalRecord(); renderFeedback(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="worker-page">

          <div class="page-header">
            <h1 class="page-title">ثبت کار امروز</h1>
            <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
          </div>

          <div class="search-card">
            <label class="search-label">جستجوی خدمت</label>

            <div class="search-input-wrap">
              <div class="search-icon">🔍</div>
              <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
            </div>

            <div class="service-results" id="serviceResults"></div>
          </div>

          <div class="summary-wrap">
            <div class="summary-card">
              <span>مبلغ امروز</span>
              <strong id="todayAmount">۰ تومان</strong>
            </div>

            <div class="summary-card">
              <span>تعداد امروز</span>
              <strong id="todayCount">۰</strong>
            </div>
          </div>

          <div class="personal-record-card">
            <div class="record-icon">🏆</div>
            <div class="record-content">
              <span>رکورد روزانه تو</span>
              <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
              <small id="recordMessage"></small>
            </div>
          </div>

          <div class="feedback-card">
            <div class="feedback-title">آخرین بازخورد عملکرد</div>
            <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
            <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
            <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
          </div>

          <div class="form-card" id="serviceForm">
            <div class="selected-service">
              <span>خدمت انتخاب شده</span>
              <strong id="selectedServiceName">---</strong>
            </div>

            <div class="form-grid">
              <div class="field">
                <label>تعداد</label>
                <input type="number" id="serviceCount" min="1" value="1" />
              </div>

              <div class="field">
                <label>مقدار / مبلغ واحد</label>
                <input type="number" id="servicePrice" min="0" />
              </div>
            </div>

            <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

            <div class="details-box" id="detailsBox">
              <div class="field">
                <label>توضیحات</label>
                <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
              </div>
            </div>

            <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
          </div>

          <div class="records-card">
            <div class="records-title">
              <strong>ثبت‌های امروز</strong>
              <span id="recordsCountText">۰ مورد</span>
            </div>

            <div id="recordsList">
              <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
            </div>
          </div>

        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  * {
    box-sizing: border-box;
  }

  .worker-page {
    max-width: 520px;
    margin: 0 auto;
  }

  .page-header {
    margin-bottom: 14px;
  }

  .page-title {
    font-size: 18px;
    font-weight: 900;
    margin: 0 0 5px;
    color: #111827;
  }

  .page-subtitle {
    font-size: 12px;
    color: #6b7280;
    margin: 0;
    line-height: 1.8;
  }

  .search-card,
  .form-card,
  .records-card {
    background: #ffffff;
    border-radius: 20px;
    padding: 13px;
    box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
    margin-bottom: 13px;
    border: 1px solid #e5e7eb;
  }

  .search-label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    margin-bottom: 8px;
    color: #374151;
  }

  .search-input-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
    background: #f9fafb;
    border: 2px solid #2563eb;
    border-radius: 15px;
    padding: 10px 12px;
  }

  .search-icon {
    font-size: 17px;
  }

  #serviceSearch {
    width: 100%;
    border: none;
    outline: none;
    background: transparent;
    font-size: 14px;
    font-weight: 700;
    color: #111827;
  }

  #serviceSearch::placeholder {
    color: #9ca3af;
    font-weight: 500;
  }

  .service-results {
    margin-top: 10px;
    display: none;
  }

  .service-result-item {
    background: #f8fafc;
    border: 1px solid #e5e7eb;
    border-radius: 13px;
    padding: 10px;
    margin-bottom: 7px;
    cursor: pointer;
  }

  .service-result-item:hover {
    background: #eef2ff;
    border-color: #c7d2fe;
  }

  .service-result-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 3px;
  }

  .service-result-price {
    font-size: 11px;
    color: #6b7280;
  }

  .summary-wrap {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 12px;
  }

  .summary-wrap .summary-card {
    background: #ffffff;
    color: #111827;
    border-radius: 17px;
    padding: 12px;
    box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
    border: 1px solid #e5e7eb;
  }

  .summary-wrap .summary-card span {
    display: block;
    color: #6b7280;
    font-size: 11px;
    font-weight: 700;
    margin-bottom: 6px;
  }

  .summary-wrap .summary-card strong {
    display: block;
    color: #111827;
    font-size: 15px;
    font-weight: 900;
  }

  #todayAmount {
    color: #16a34a;
  }

  .personal-record-card {
    background: linear-gradient(135deg, #fff7ed, #fffbeb);
    border: 1px solid #fed7aa;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    display: flex;
    align-items: center;
    gap: 11px;
    box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
  }

  .record-icon {
    width: 42px;
    height: 42px;
    border-radius: 14px;
    background: #ffedd5;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 21px;
    flex-shrink: 0;
  }

  .record-content {
    flex: 1;
  }

  .record-content span {
    display: block;
    color: #9a3412;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content strong {
    display: block;
    color: #111827;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .record-content small {
    display: none;
    color: #92400e;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.7;
  }

  .feedback-card {
    background: linear-gradient(135deg, #eff6ff, #f8fafc);
    border: 1px solid #bfdbfe;
    border-radius: 18px;
    padding: 12px 13px;
    margin-bottom: 13px;
    box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
  }

  .feedback-title {
    font-size: 12px;
    font-weight: 900;
    color: #1d4ed8;
    margin-bottom: 7px;
  }

  .feedback-main {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
    margin-bottom: 5px;
  }

  .feedback-sub {
    font-size: 11px;
    line-height: 1.8;
    color: #4b5563;
  }

  .feedback-badge {
    display: inline-block;
    margin-top: 8px;
    padding: 5px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 900;
  }

  .feedback-badge.positive {
    background: #dbeafe;
    color: #1d4ed8;
  }

  .feedback-badge.negative {
    background: #fef3c7;
    color: #92400e;
  }

  .feedback-badge.neutral {
    background: #e5e7eb;
    color: #374151;
  }

  .form-card {
    display: none;
  }

  .selected-service {
    background: #eff6ff;
    border: 1px solid #bfdbfe;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 12px;
  }

  .selected-service span {
    display: block;
    color: #1d4ed8;
    font-size: 11px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .selected-service strong {
    display: block;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .field {
    margin-bottom: 10px;
  }

  .field label {
    display: block;
    font-size: 11px;
    font-weight: 900;
    color: #374151;
    margin-bottom: 6px;
  }

  .field input,
  .field textarea {
    width: 100%;
    border: 1px solid #d1d5db;
    outline: none;
    background: #f9fafb;
    border-radius: 13px;
    padding: 10px;
    font-size: 13px;
    font-family: inherit;
  }

  .field input:focus,
  .field textarea:focus {
    border-color: #2563eb;
    background: #ffffff;
  }

  .field textarea {
    min-height: 75px;
    resize: vertical;
    line-height: 1.8;
  }

  .details-toggle {
    width: 100%;
    border: none;
    background: #f3f4f6;
    color: #374151;
    border-radius: 13px;
    padding: 10px;
    font-size: 12px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
    margin-bottom: 10px;
  }

  .details-box {
    display: none;
  }

  .submit-btn {
    width: 100%;
    border: none;
    background: #2563eb;
    color: #ffffff;
    border-radius: 15px;
    padding: 12px;
    font-size: 14px;
    font-weight: 900;
    font-family: inherit;
    cursor: pointer;
  }

  .records-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }

  .records-title strong {
    font-size: 14px;
    font-weight: 900;
    color: #111827;
  }

  .records-title span {
    font-size: 11px;
    color: #6b7280;
    font-weight: 700;
  }

  .empty-records {
    background: #f9fafb;
    color: #6b7280;
    text-align: center;
    border-radius: 14px;
    padding: 16px 10px;
    font-size: 12px;
    line-height: 1.8;
  }

  .record-item {
    border: 1px solid #e5e7eb;
    border-radius: 15px;
    padding: 11px;
    margin-bottom: 9px;
    background: #ffffff;
  }

  .record-item:last-child {
    margin-bottom: 0;
  }

  .record-top {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 7px;
  }

  .record-name {
    font-size: 13px;
    font-weight: 900;
    color: #111827;
  }

  .record-time {
    font-size: 10px;
    color: #9ca3af;
    white-space: nowrap;
  }

  .record-info {
    font-size: 11px;
    color: #4b5563;
    line-height: 1.9;
  }

  .record-total {
    margin-top: 6px;
    font-size: 12px;
    font-weight: 900;
    color: #16a34a;
  }

  .record-desc {
    margin-top: 5px;
    color: #6b7280;
    font-size: 11px;
    line-height: 1.8;
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }

  @media (max-width: 380px) {
    .summary-wrap .summary-card strong {
      font-size: 14px;
    }
  }
</style>

<script>
(function(){
  let selectedService = null;
  let records = [];

  let latestFeedback = {
    type: "positive",
    title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
    description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
    badge: "۸۰٪"
  };

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const services = [
    { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
    { name: "جوشکاری", price: 200000 },
    { name: "نجاری", price: 180000 }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const toast = document.getElementById("toast");

  const serviceSearch = document.getElementById("serviceSearch");
  const serviceResults = document.getElementById("serviceResults");
  const serviceForm = document.getElementById("serviceForm");
  const selectedServiceName = document.getElementById("selectedServiceName");
  const serviceCount = document.getElementById("serviceCount");
  const servicePrice = document.getElementById("servicePrice");
  const serviceDescription = document.getElementById("serviceDescription");
  const submitService = document.getElementById("submitService");
  const todayAmount = document.getElementById("todayAmount");
  const todayCount = document.getElementById("todayCount");
  const recordsList = document.getElementById("recordsList");
  const recordsCountText = document.getElementById("recordsCountText");
  const detailsToggle = document.getElementById("detailsToggle");
  const detailsBox = document.getElementById("detailsBox");
  const bestRecordText = document.getElementById("bestRecordText");
  const recordMessage = document.getElementById("recordMessage");
  const feedbackMain = document.getElementById("feedbackMain");
  const feedbackSub = document.getElementById("feedbackSub");
  const feedbackBadge = document.getElementById("feedbackBadge");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function toPersianNumber(value) {
    return Number(value || 0).toLocaleString("fa-IR");
  }

  function formatToman(value) {
    return toPersianNumber(value) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function showResults(keyword) {
    const text = keyword.trim();
    serviceResults.innerHTML = "";

    if (!text) {
      serviceResults.style.display = "none";
      return;
    }

    const filtered = services.filter(function(service) {
      return service.name.includes(text);
    });

    if (filtered.length === 0) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
        <div class="service-result-price">برای انتخاب این مورد بزنید</div>
      `;
      item.addEventListener("click", function() {
        selectService({ name: text, price: 0 });
      });
      serviceResults.appendChild(item);
      serviceResults.style.display = "block";
      return;
    }

    filtered.forEach(function(service) {
      const item = document.createElement("div");
      item.className = "service-result-item";
      item.innerHTML = `
        <div class="service-result-name">${service.name}</div>
        <div class="service-result-price">${formatToman(service.price)}</div>
      `;
      item.addEventListener("click", function() {
        selectService(service);
      });
      serviceResults.appendChild(item);
    });

    serviceResults.style.display = "block";
  }

  function selectService(service) {
    selectedService = service;
    selectedServiceName.textContent = service.name;
    serviceSearch.value = service.name;
    servicePrice.value = service.price || "";
    serviceCount.value = 1;
    serviceDescription.value = "";
    serviceResults.style.display = "none";
    serviceForm.style.display = "block";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";

    setTimeout(function() {
      serviceCount.focus();
    }, 100);
  }

  function updateSummary() {
    const totalAmount = records.reduce(function(sum, item) {
      return sum + item.total;
    }, 0);

    const totalCount = records.reduce(function(sum, item) {
      return sum + item.count;
    }, 0);

    todayAmount.textContent = formatToman(totalAmount);
    todayCount.textContent = toPersianNumber(totalCount);
  }

  function updatePersonalRecord() {
    bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
    recordMessage.textContent = "";
  }

  function renderRecords() {
    recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

    if (records.length === 0) {
      recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
      return;
    }

    recordsList.innerHTML = "";

    const reversed = records.slice().reverse();
    reversed.forEach(function(item) {
      const div = document.createElement("div");
      div.className = "record-item";
      div.innerHTML = `
        <div class="record-top">
          <div class="record-name">${item.name}</div>
          <div class="record-time">${item.time}</div>
        </div>
        <div class="record-info">
          تعداد: ${toPersianNumber(item.count)} |
          مبلغ واحد: ${formatToman(item.price)}
        </div>
        <div class="record-total">
          جمع: ${formatToman(item.total)}
        </div>
        ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
      `;
      recordsList.appendChild(div);
    });
  }

  function renderFeedback() {
    feedbackMain.textContent = latestFeedback.title;
    feedbackSub.textContent = latestFeedback.description;
    feedbackBadge.textContent = latestFeedback.badge;
    feedbackBadge.className = "feedback-badge " + latestFeedback.type;
  }

  function submitRecord() {
    if (!selectedService) {
      alert("اول یک خدمت را انتخاب کن.");
      return;
    }

    const count = parseInt(serviceCount.value, 10);
    const price = parseInt(servicePrice.value, 10);
    const description = serviceDescription.value.trim();

    if (!count || count <= 0) {
      alert("تعداد را درست وارد کن.");
      return;
    }

    if (isNaN(price) || price < 0) {
      alert("مبلغ را درست وارد کن.");
      return;
    }

    const total = count * price;
    const now = new Date();

    records.push({
      name: selectedService.name,
      count: count,
      price: price,
      total: total,
      description: description,
      time: now.toLocaleTimeString("fa-IR", {
        hour: "2-digit",
        minute: "2-digit"
      })
    });

    renderRecords();
    updateSummary();

    selectedService = null;
    serviceSearch.value = "";
    serviceCount.value = 1;
    servicePrice.value = "";
    serviceDescription.value = "";
    selectedServiceName.textContent = "---";
    serviceForm.style.display = "none";
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
    serviceSearch.focus();

    showToast("ثبت جدید اضافه شد");
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmountValue = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmountValue);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderRecords();
    updateSummary();
    updatePersonalRecord();
    renderFeedback();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function() {
    showResults(serviceSearch.value);
  });

  detailsToggle.addEventListener("click", function() {
    if (detailsBox.style.display === "block") {
      detailsBox.style.display = "none";
      detailsToggle.textContent = "افزودن توضیحات اختیاری";
    } else {
      detailsBox.style.display = "block";
      detailsToggle.textContent = "بستن توضیحات";
    }
  });

  submitService.addEventListener("click", submitRecord);

  renderAll();
})();
</script>
۸۸۸
TEXT - 2026-05-12 00:34:09
متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم. همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم. ```html <!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ثبت کار امروز</title> <style> * { box-sizing: border-box; } body { margin: 0; padding: 14px; background: #f3f6fb; font-family: Tahoma, Arial, sans-serif; color: #111827; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-card { background: #ffffff; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: none; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width: 380px) { body { padding: 10px; } .summary-card strong { font-size: 14px; } } </style> </head> <body> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage"></small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> <script> const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; let selectedService = null; let records = []; let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || { count: 0, date: null }; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function getTodayDateKey() { const now = new Date(); return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate(); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); updatePersonalRecord(totalCount); } function updatePersonalRecord(totalCount) { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = ""; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function saveFeedback(feedback) { latestFeedback = feedback; localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback)); renderFeedback(); } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); } serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); updateSummary(); renderRecords(); renderFeedback(); </script> </body> </html> ``` متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم. همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم.
متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم.  
همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم.

```html
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ثبت کار امروز</title>

<style>
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 14px;
  background: #f3f6fb;
  font-family: Tahoma, Arial, sans-serif;
  color: #111827;
}

.worker-page {
  max-width: 520px;
  margin: 0 auto;
}

.page-header {
  margin-bottom: 14px;
}

.page-title {
  font-size: 18px;
  font-weight: 900;
  margin: 0 0 5px;
  color: #111827;
}

.page-subtitle {
  font-size: 12px;
  color: #6b7280;
  margin: 0;
  line-height: 1.8;
}

.search-card,
.form-card,
.records-card {
  background: #ffffff;
  border-radius: 20px;
  padding: 13px;
  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
  margin-bottom: 13px;
  border: 1px solid #e5e7eb;
}

.search-label {
  display: block;
  font-size: 12px;
  font-weight: 900;
  margin-bottom: 8px;
  color: #374151;
}

.search-input-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #f9fafb;
  border: 2px solid #2563eb;
  border-radius: 15px;
  padding: 10px 12px;
}

.search-icon {
  font-size: 17px;
}

#serviceSearch {
  width: 100%;
  border: none;
  outline: none;
  background: transparent;
  font-size: 14px;
  font-weight: 700;
  color: #111827;
}

#serviceSearch::placeholder {
  color: #9ca3af;
  font-weight: 500;
}

.service-results {
  margin-top: 10px;
  display: none;
}

.service-result-item {
  background: #f8fafc;
  border: 1px solid #e5e7eb;
  border-radius: 13px;
  padding: 10px;
  margin-bottom: 7px;
  cursor: pointer;
}

.service-result-item:hover {
  background: #eef2ff;
  border-color: #c7d2fe;
}

.service-result-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 3px;
}

.service-result-price {
  font-size: 11px;
  color: #6b7280;
}

.summary-wrap {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 12px;
}

.summary-card {
  background: #ffffff;
  border-radius: 17px;
  padding: 12px;
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
  border: 1px solid #e5e7eb;
}

.summary-card span {
  display: block;
  color: #6b7280;
  font-size: 11px;
  font-weight: 700;
  margin-bottom: 6px;
}

.summary-card strong {
  display: block;
  color: #111827;
  font-size: 15px;
  font-weight: 900;
}

#todayAmount {
  color: #16a34a;
}

.personal-record-card {
  background: linear-gradient(135deg, #fff7ed, #fffbeb);
  border: 1px solid #fed7aa;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  display: flex;
  align-items: center;
  gap: 11px;
  box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
}

.record-icon {
  width: 42px;
  height: 42px;
  border-radius: 14px;
  background: #ffedd5;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 21px;
  flex-shrink: 0;
}

.record-content {
  flex: 1;
}

.record-content span {
  display: block;
  color: #9a3412;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content strong {
  display: block;
  color: #111827;
  font-size: 13px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content small {
  display: none;
  color: #92400e;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.7;
}

.feedback-card {
  background: linear-gradient(135deg, #eff6ff, #f8fafc);
  border: 1px solid #bfdbfe;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
}

.feedback-title {
  font-size: 12px;
  font-weight: 900;
  color: #1d4ed8;
  margin-bottom: 7px;
}

.feedback-main {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 5px;
}

.feedback-sub {
  font-size: 11px;
  line-height: 1.8;
  color: #4b5563;
}

.feedback-badge {
  display: inline-block;
  margin-top: 8px;
  padding: 5px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 900;
}

.feedback-badge.positive {
  background: #dbeafe;
  color: #1d4ed8;
}

.feedback-badge.negative {
  background: #fef3c7;
  color: #92400e;
}

.feedback-badge.neutral {
  background: #e5e7eb;
  color: #374151;
}

.form-card {
  display: none;
}

.selected-service {
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 12px;
}

.selected-service span {
  display: block;
  color: #1d4ed8;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.selected-service strong {
  display: block;
  color: #111827;
  font-size: 14px;
  font-weight: 900;
}

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.field {
  margin-bottom: 10px;
}

.field label {
  display: block;
  font-size: 11px;
  font-weight: 900;
  color: #374151;
  margin-bottom: 6px;
}

.field input,
.field textarea {
  width: 100%;
  border: 1px solid #d1d5db;
  outline: none;
  background: #f9fafb;
  border-radius: 13px;
  padding: 10px;
  font-size: 13px;
  font-family: inherit;
}

.field input:focus,
.field textarea:focus {
  border-color: #2563eb;
  background: #ffffff;
}

.field textarea {
  min-height: 75px;
  resize: vertical;
  line-height: 1.8;
}

.details-toggle {
  width: 100%;
  border: none;
  background: #f3f4f6;
  color: #374151;
  border-radius: 13px;
  padding: 10px;
  font-size: 12px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
  margin-bottom: 10px;
}

.details-box {
  display: none;
}

.submit-btn {
  width: 100%;
  border: none;
  background: #2563eb;
  color: #ffffff;
  border-radius: 15px;
  padding: 12px;
  font-size: 14px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
}

.records-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.records-title strong {
  font-size: 14px;
  font-weight: 900;
  color: #111827;
}

.records-title span {
  font-size: 11px;
  color: #6b7280;
  font-weight: 700;
}

.empty-records {
  background: #f9fafb;
  color: #6b7280;
  text-align: center;
  border-radius: 14px;
  padding: 16px 10px;
  font-size: 12px;
  line-height: 1.8;
}

.record-item {
  border: 1px solid #e5e7eb;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 9px;
  background: #ffffff;
}

.record-item:last-child {
  margin-bottom: 0;
}

.record-top {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 7px;
}

.record-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
}

.record-time {
  font-size: 10px;
  color: #9ca3af;
  white-space: nowrap;
}

.record-info {
  font-size: 11px;
  color: #4b5563;
  line-height: 1.9;
}

.record-total {
  margin-top: 6px;
  font-size: 12px;
  font-weight: 900;
  color: #16a34a;
}

.record-desc {
  margin-top: 5px;
  color: #6b7280;
  font-size: 11px;
  line-height: 1.8;
}

@media (max-width: 380px) {
  body {
    padding: 10px;
  }

  .summary-card strong {
    font-size: 14px;
  }
}
</style>
</head>
<body>

<div class="worker-page">

  <div class="page-header">
    <h1 class="page-title">ثبت کار امروز</h1>
    <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
  </div>

  <div class="search-card">
    <label class="search-label">جستجوی خدمت</label>

    <div class="search-input-wrap">
      <div class="search-icon">🔍</div>
      <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
    </div>

    <div class="service-results" id="serviceResults"></div>
  </div>

  <div class="summary-wrap">
    <div class="summary-card">
      <span>مبلغ امروز</span>
      <strong id="todayAmount">۰ تومان</strong>
    </div>

    <div class="summary-card">
      <span>تعداد امروز</span>
      <strong id="todayCount">۰</strong>
    </div>
  </div>

  <div class="personal-record-card">
    <div class="record-icon">🏆</div>
    <div class="record-content">
      <span>رکورد روزانه تو</span>
      <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
      <small id="recordMessage"></small>
    </div>
  </div>

  <div class="feedback-card">
    <div class="feedback-title">آخرین بازخورد عملکرد</div>
    <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
    <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
    <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
  </div>

  <div class="form-card" id="serviceForm">
    <div class="selected-service">
      <span>خدمت انتخاب شده</span>
      <strong id="selectedServiceName">---</strong>
    </div>

    <div class="form-grid">
      <div class="field">
        <label>تعداد</label>
        <input type="number" id="serviceCount" min="1" value="1" />
      </div>

      <div class="field">
        <label>مقدار / مبلغ واحد</label>
        <input type="number" id="servicePrice" min="0" />
      </div>
    </div>

    <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

    <div class="details-box" id="detailsBox">
      <div class="field">
        <label>توضیحات</label>
        <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
      </div>
    </div>

    <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
  </div>

  <div class="records-card">
    <div class="records-title">
      <strong>ثبت‌های امروز</strong>
      <span id="recordsCountText">۰ مورد</span>
    </div>

    <div id="recordsList">
      <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
    </div>
  </div>

</div>

<script>
const services = [
  { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
  { name: "جوشکاری", price: 200000 },
  { name: "نجاری", price: 180000 }
];

let selectedService = null;
let records = [];

let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || {
  count: 0,
  date: null
};

let latestFeedback = {
  type: "positive",
  title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
  description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
  badge: "۸۰٪"
};

const serviceSearch = document.getElementById("serviceSearch");
const serviceResults = document.getElementById("serviceResults");
const serviceForm = document.getElementById("serviceForm");
const selectedServiceName = document.getElementById("selectedServiceName");
const serviceCount = document.getElementById("serviceCount");
const servicePrice = document.getElementById("servicePrice");
const serviceDescription = document.getElementById("serviceDescription");
const submitService = document.getElementById("submitService");
const todayAmount = document.getElementById("todayAmount");
const todayCount = document.getElementById("todayCount");
const recordsList = document.getElementById("recordsList");
const recordsCountText = document.getElementById("recordsCountText");
const detailsToggle = document.getElementById("detailsToggle");
const detailsBox = document.getElementById("detailsBox");
const bestRecordText = document.getElementById("bestRecordText");
const recordMessage = document.getElementById("recordMessage");
const feedbackMain = document.getElementById("feedbackMain");
const feedbackSub = document.getElementById("feedbackSub");
const feedbackBadge = document.getElementById("feedbackBadge");

function toPersianNumber(value) {
  return Number(value || 0).toLocaleString("fa-IR");
}

function formatToman(value) {
  return toPersianNumber(value) + " تومان";
}

function getTodayDateKey() {
  const now = new Date();
  return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate();
}

function showResults(keyword) {
  const text = keyword.trim();
  serviceResults.innerHTML = "";

  if (!text) {
    serviceResults.style.display = "none";
    return;
  }

  const filtered = services.filter(function(service) {
    return service.name.includes(text);
  });

  if (filtered.length === 0) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
      <div class="service-result-price">برای انتخاب این مورد بزنید</div>
    `;
    item.addEventListener("click", function() {
      selectService({ name: text, price: 0 });
    });
    serviceResults.appendChild(item);
    serviceResults.style.display = "block";
    return;
  }

  filtered.forEach(function(service) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">${service.name}</div>
      <div class="service-result-price">${formatToman(service.price)}</div>
    `;
    item.addEventListener("click", function() {
      selectService(service);
    });
    serviceResults.appendChild(item);
  });

  serviceResults.style.display = "block";
}

function selectService(service) {
  selectedService = service;
  selectedServiceName.textContent = service.name;
  serviceSearch.value = service.name;
  servicePrice.value = service.price || "";
  serviceCount.value = 1;
  serviceDescription.value = "";
  serviceResults.style.display = "none";
  serviceForm.style.display = "block";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";

  setTimeout(function() {
    serviceCount.focus();
  }, 100);
}

function updateSummary() {
  const totalAmount = records.reduce(function(sum, item) {
    return sum + item.total;
  }, 0);

  const totalCount = records.reduce(function(sum, item) {
    return sum + item.count;
  }, 0);

  todayAmount.textContent = formatToman(totalAmount);
  todayCount.textContent = toPersianNumber(totalCount);

  updatePersonalRecord(totalCount);
}

function updatePersonalRecord(totalCount) {
  bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
  recordMessage.textContent = "";
}

function renderRecords() {
  recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

  if (records.length === 0) {
    recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
    return;
  }

  recordsList.innerHTML = "";

  const reversed = records.slice().reverse();
  reversed.forEach(function(item) {
    const div = document.createElement("div");
    div.className = "record-item";
    div.innerHTML = `
      <div class="record-top">
        <div class="record-name">${item.name}</div>
        <div class="record-time">${item.time}</div>
      </div>
      <div class="record-info">
        تعداد: ${toPersianNumber(item.count)} |
        مبلغ واحد: ${formatToman(item.price)}
      </div>
      <div class="record-total">
        جمع: ${formatToman(item.total)}
      </div>
      ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
    `;
    recordsList.appendChild(div);
  });
}

function renderFeedback() {
  feedbackMain.textContent = latestFeedback.title;
  feedbackSub.textContent = latestFeedback.description;
  feedbackBadge.textContent = latestFeedback.badge;
  feedbackBadge.className = "feedback-badge " + latestFeedback.type;
}

function saveFeedback(feedback) {
  latestFeedback = feedback;
  localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback));
  renderFeedback();
}

function submitRecord() {
  if (!selectedService) {
    alert("اول یک خدمت را انتخاب کن.");
    return;
  }

  const count = parseInt(serviceCount.value, 10);
  const price = parseInt(servicePrice.value, 10);
  const description = serviceDescription.value.trim();

  if (!count || count <= 0) {
    alert("تعداد را درست وارد کن.");
    return;
  }

  if (isNaN(price) || price < 0) {
    alert("مبلغ را درست وارد کن.");
    return;
  }

  const total = count * price;
  const now = new Date();

  records.push({
    name: selectedService.name,
    count: count,
    price: price,
    total: total,
    description: description,
    time: now.toLocaleTimeString("fa-IR", {
      hour: "2-digit",
      minute: "2-digit"
    })
  });

  renderRecords();
  updateSummary();

  selectedService = null;
  serviceSearch.value = "";
  serviceCount.value = 1;
  servicePrice.value = "";
  serviceDescription.value = "";
  selectedServiceName.textContent = "---";
  serviceForm.style.display = "none";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";
  serviceSearch.focus();
}

serviceSearch.addEventListener("input", function() {
  showResults(serviceSearch.value);
});

detailsToggle.addEventListener("click", function() {
  if (detailsBox.style.display === "block") {
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
  } else {
    detailsBox.style.display = "block";
    detailsToggle.textContent = "بستن توضیحات";
  }
});

submitService.addEventListener("click", submitRecord);

updateSummary();
renderRecords();
renderFeedback();
</script>

</body>
</html>
```

متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم.  
همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم.
۸۸۸
TEXT - 2026-05-12 00:28:05
متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم. همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم. ```html <!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ثبت کار امروز</title> <style> * { box-sizing: border-box; } body { margin: 0; padding: 14px; background: #f3f6fb; font-family: Tahoma, Arial, sans-serif; color: #111827; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-card { background: #ffffff; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: none; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width: 380px) { body { padding: 10px; } .summary-card strong { font-size: 14px; } } </style> </head> <body> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage"></small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> <script> const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; let selectedService = null; let records = []; let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || { count: 0, date: null }; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function getTodayDateKey() { const now = new Date(); return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate(); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); updatePersonalRecord(totalCount); } function updatePersonalRecord(totalCount) { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = ""; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function saveFeedback(feedback) { latestFeedback = feedback; localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback)); renderFeedback(); } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); } serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); updateSummary(); renderRecords(); renderFeedback(); </script> </body> </html> ``` متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم. همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم.
متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم.  
همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم.

```html
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ثبت کار امروز</title>

<style>
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 14px;
  background: #f3f6fb;
  font-family: Tahoma, Arial, sans-serif;
  color: #111827;
}

.worker-page {
  max-width: 520px;
  margin: 0 auto;
}

.page-header {
  margin-bottom: 14px;
}

.page-title {
  font-size: 18px;
  font-weight: 900;
  margin: 0 0 5px;
  color: #111827;
}

.page-subtitle {
  font-size: 12px;
  color: #6b7280;
  margin: 0;
  line-height: 1.8;
}

.search-card,
.form-card,
.records-card {
  background: #ffffff;
  border-radius: 20px;
  padding: 13px;
  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
  margin-bottom: 13px;
  border: 1px solid #e5e7eb;
}

.search-label {
  display: block;
  font-size: 12px;
  font-weight: 900;
  margin-bottom: 8px;
  color: #374151;
}

.search-input-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #f9fafb;
  border: 2px solid #2563eb;
  border-radius: 15px;
  padding: 10px 12px;
}

.search-icon {
  font-size: 17px;
}

#serviceSearch {
  width: 100%;
  border: none;
  outline: none;
  background: transparent;
  font-size: 14px;
  font-weight: 700;
  color: #111827;
}

#serviceSearch::placeholder {
  color: #9ca3af;
  font-weight: 500;
}

.service-results {
  margin-top: 10px;
  display: none;
}

.service-result-item {
  background: #f8fafc;
  border: 1px solid #e5e7eb;
  border-radius: 13px;
  padding: 10px;
  margin-bottom: 7px;
  cursor: pointer;
}

.service-result-item:hover {
  background: #eef2ff;
  border-color: #c7d2fe;
}

.service-result-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 3px;
}

.service-result-price {
  font-size: 11px;
  color: #6b7280;
}

.summary-wrap {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 12px;
}

.summary-card {
  background: #ffffff;
  border-radius: 17px;
  padding: 12px;
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
  border: 1px solid #e5e7eb;
}

.summary-card span {
  display: block;
  color: #6b7280;
  font-size: 11px;
  font-weight: 700;
  margin-bottom: 6px;
}

.summary-card strong {
  display: block;
  color: #111827;
  font-size: 15px;
  font-weight: 900;
}

#todayAmount {
  color: #16a34a;
}

.personal-record-card {
  background: linear-gradient(135deg, #fff7ed, #fffbeb);
  border: 1px solid #fed7aa;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  display: flex;
  align-items: center;
  gap: 11px;
  box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
}

.record-icon {
  width: 42px;
  height: 42px;
  border-radius: 14px;
  background: #ffedd5;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 21px;
  flex-shrink: 0;
}

.record-content {
  flex: 1;
}

.record-content span {
  display: block;
  color: #9a3412;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content strong {
  display: block;
  color: #111827;
  font-size: 13px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content small {
  display: none;
  color: #92400e;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.7;
}

.feedback-card {
  background: linear-gradient(135deg, #eff6ff, #f8fafc);
  border: 1px solid #bfdbfe;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
}

.feedback-title {
  font-size: 12px;
  font-weight: 900;
  color: #1d4ed8;
  margin-bottom: 7px;
}

.feedback-main {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 5px;
}

.feedback-sub {
  font-size: 11px;
  line-height: 1.8;
  color: #4b5563;
}

.feedback-badge {
  display: inline-block;
  margin-top: 8px;
  padding: 5px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 900;
}

.feedback-badge.positive {
  background: #dbeafe;
  color: #1d4ed8;
}

.feedback-badge.negative {
  background: #fef3c7;
  color: #92400e;
}

.feedback-badge.neutral {
  background: #e5e7eb;
  color: #374151;
}

.form-card {
  display: none;
}

.selected-service {
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 12px;
}

.selected-service span {
  display: block;
  color: #1d4ed8;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.selected-service strong {
  display: block;
  color: #111827;
  font-size: 14px;
  font-weight: 900;
}

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.field {
  margin-bottom: 10px;
}

.field label {
  display: block;
  font-size: 11px;
  font-weight: 900;
  color: #374151;
  margin-bottom: 6px;
}

.field input,
.field textarea {
  width: 100%;
  border: 1px solid #d1d5db;
  outline: none;
  background: #f9fafb;
  border-radius: 13px;
  padding: 10px;
  font-size: 13px;
  font-family: inherit;
}

.field input:focus,
.field textarea:focus {
  border-color: #2563eb;
  background: #ffffff;
}

.field textarea {
  min-height: 75px;
  resize: vertical;
  line-height: 1.8;
}

.details-toggle {
  width: 100%;
  border: none;
  background: #f3f4f6;
  color: #374151;
  border-radius: 13px;
  padding: 10px;
  font-size: 12px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
  margin-bottom: 10px;
}

.details-box {
  display: none;
}

.submit-btn {
  width: 100%;
  border: none;
  background: #2563eb;
  color: #ffffff;
  border-radius: 15px;
  padding: 12px;
  font-size: 14px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
}

.records-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.records-title strong {
  font-size: 14px;
  font-weight: 900;
  color: #111827;
}

.records-title span {
  font-size: 11px;
  color: #6b7280;
  font-weight: 700;
}

.empty-records {
  background: #f9fafb;
  color: #6b7280;
  text-align: center;
  border-radius: 14px;
  padding: 16px 10px;
  font-size: 12px;
  line-height: 1.8;
}

.record-item {
  border: 1px solid #e5e7eb;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 9px;
  background: #ffffff;
}

.record-item:last-child {
  margin-bottom: 0;
}

.record-top {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 7px;
}

.record-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
}

.record-time {
  font-size: 10px;
  color: #9ca3af;
  white-space: nowrap;
}

.record-info {
  font-size: 11px;
  color: #4b5563;
  line-height: 1.9;
}

.record-total {
  margin-top: 6px;
  font-size: 12px;
  font-weight: 900;
  color: #16a34a;
}

.record-desc {
  margin-top: 5px;
  color: #6b7280;
  font-size: 11px;
  line-height: 1.8;
}

@media (max-width: 380px) {
  body {
    padding: 10px;
  }

  .summary-card strong {
    font-size: 14px;
  }
}
</style>
</head>
<body>

<div class="worker-page">

  <div class="page-header">
    <h1 class="page-title">ثبت کار امروز</h1>
    <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
  </div>

  <div class="search-card">
    <label class="search-label">جستجوی خدمت</label>

    <div class="search-input-wrap">
      <div class="search-icon">🔍</div>
      <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
    </div>

    <div class="service-results" id="serviceResults"></div>
  </div>

  <div class="summary-wrap">
    <div class="summary-card">
      <span>مبلغ امروز</span>
      <strong id="todayAmount">۰ تومان</strong>
    </div>

    <div class="summary-card">
      <span>تعداد امروز</span>
      <strong id="todayCount">۰</strong>
    </div>
  </div>

  <div class="personal-record-card">
    <div class="record-icon">🏆</div>
    <div class="record-content">
      <span>رکورد روزانه تو</span>
      <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
      <small id="recordMessage"></small>
    </div>
  </div>

  <div class="feedback-card">
    <div class="feedback-title">آخرین بازخورد عملکرد</div>
    <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
    <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
    <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
  </div>

  <div class="form-card" id="serviceForm">
    <div class="selected-service">
      <span>خدمت انتخاب شده</span>
      <strong id="selectedServiceName">---</strong>
    </div>

    <div class="form-grid">
      <div class="field">
        <label>تعداد</label>
        <input type="number" id="serviceCount" min="1" value="1" />
      </div>

      <div class="field">
        <label>مقدار / مبلغ واحد</label>
        <input type="number" id="servicePrice" min="0" />
      </div>
    </div>

    <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

    <div class="details-box" id="detailsBox">
      <div class="field">
        <label>توضیحات</label>
        <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
      </div>
    </div>

    <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
  </div>

  <div class="records-card">
    <div class="records-title">
      <strong>ثبت‌های امروز</strong>
      <span id="recordsCountText">۰ مورد</span>
    </div>

    <div id="recordsList">
      <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
    </div>
  </div>

</div>

<script>
const services = [
  { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
  { name: "جوشکاری", price: 200000 },
  { name: "نجاری", price: 180000 }
];

let selectedService = null;
let records = [];

let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || {
  count: 0,
  date: null
};

let latestFeedback = {
  type: "positive",
  title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
  description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
  badge: "۸۰٪"
};

const serviceSearch = document.getElementById("serviceSearch");
const serviceResults = document.getElementById("serviceResults");
const serviceForm = document.getElementById("serviceForm");
const selectedServiceName = document.getElementById("selectedServiceName");
const serviceCount = document.getElementById("serviceCount");
const servicePrice = document.getElementById("servicePrice");
const serviceDescription = document.getElementById("serviceDescription");
const submitService = document.getElementById("submitService");
const todayAmount = document.getElementById("todayAmount");
const todayCount = document.getElementById("todayCount");
const recordsList = document.getElementById("recordsList");
const recordsCountText = document.getElementById("recordsCountText");
const detailsToggle = document.getElementById("detailsToggle");
const detailsBox = document.getElementById("detailsBox");
const bestRecordText = document.getElementById("bestRecordText");
const recordMessage = document.getElementById("recordMessage");
const feedbackMain = document.getElementById("feedbackMain");
const feedbackSub = document.getElementById("feedbackSub");
const feedbackBadge = document.getElementById("feedbackBadge");

function toPersianNumber(value) {
  return Number(value || 0).toLocaleString("fa-IR");
}

function formatToman(value) {
  return toPersianNumber(value) + " تومان";
}

function getTodayDateKey() {
  const now = new Date();
  return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate();
}

function showResults(keyword) {
  const text = keyword.trim();
  serviceResults.innerHTML = "";

  if (!text) {
    serviceResults.style.display = "none";
    return;
  }

  const filtered = services.filter(function(service) {
    return service.name.includes(text);
  });

  if (filtered.length === 0) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
      <div class="service-result-price">برای انتخاب این مورد بزنید</div>
    `;
    item.addEventListener("click", function() {
      selectService({ name: text, price: 0 });
    });
    serviceResults.appendChild(item);
    serviceResults.style.display = "block";
    return;
  }

  filtered.forEach(function(service) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">${service.name}</div>
      <div class="service-result-price">${formatToman(service.price)}</div>
    `;
    item.addEventListener("click", function() {
      selectService(service);
    });
    serviceResults.appendChild(item);
  });

  serviceResults.style.display = "block";
}

function selectService(service) {
  selectedService = service;
  selectedServiceName.textContent = service.name;
  serviceSearch.value = service.name;
  servicePrice.value = service.price || "";
  serviceCount.value = 1;
  serviceDescription.value = "";
  serviceResults.style.display = "none";
  serviceForm.style.display = "block";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";

  setTimeout(function() {
    serviceCount.focus();
  }, 100);
}

function updateSummary() {
  const totalAmount = records.reduce(function(sum, item) {
    return sum + item.total;
  }, 0);

  const totalCount = records.reduce(function(sum, item) {
    return sum + item.count;
  }, 0);

  todayAmount.textContent = formatToman(totalAmount);
  todayCount.textContent = toPersianNumber(totalCount);

  updatePersonalRecord(totalCount);
}

function updatePersonalRecord(totalCount) {
  bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
  recordMessage.textContent = "";
}

function renderRecords() {
  recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

  if (records.length === 0) {
    recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
    return;
  }

  recordsList.innerHTML = "";

  const reversed = records.slice().reverse();
  reversed.forEach(function(item) {
    const div = document.createElement("div");
    div.className = "record-item";
    div.innerHTML = `
      <div class="record-top">
        <div class="record-name">${item.name}</div>
        <div class="record-time">${item.time}</div>
      </div>
      <div class="record-info">
        تعداد: ${toPersianNumber(item.count)} |
        مبلغ واحد: ${formatToman(item.price)}
      </div>
      <div class="record-total">
        جمع: ${formatToman(item.total)}
      </div>
      ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
    `;
    recordsList.appendChild(div);
  });
}

function renderFeedback() {
  feedbackMain.textContent = latestFeedback.title;
  feedbackSub.textContent = latestFeedback.description;
  feedbackBadge.textContent = latestFeedback.badge;
  feedbackBadge.className = "feedback-badge " + latestFeedback.type;
}

function saveFeedback(feedback) {
  latestFeedback = feedback;
  localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback));
  renderFeedback();
}

function submitRecord() {
  if (!selectedService) {
    alert("اول یک خدمت را انتخاب کن.");
    return;
  }

  const count = parseInt(serviceCount.value, 10);
  const price = parseInt(servicePrice.value, 10);
  const description = serviceDescription.value.trim();

  if (!count || count <= 0) {
    alert("تعداد را درست وارد کن.");
    return;
  }

  if (isNaN(price) || price < 0) {
    alert("مبلغ را درست وارد کن.");
    return;
  }

  const total = count * price;
  const now = new Date();

  records.push({
    name: selectedService.name,
    count: count,
    price: price,
    total: total,
    description: description,
    time: now.toLocaleTimeString("fa-IR", {
      hour: "2-digit",
      minute: "2-digit"
    })
  });

  renderRecords();
  updateSummary();

  selectedService = null;
  serviceSearch.value = "";
  serviceCount.value = 1;
  servicePrice.value = "";
  serviceDescription.value = "";
  selectedServiceName.textContent = "---";
  serviceForm.style.display = "none";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";
  serviceSearch.focus();
}

serviceSearch.addEventListener("input", function() {
  showResults(serviceSearch.value);
});

detailsToggle.addEventListener("click", function() {
  if (detailsBox.style.display === "block") {
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
  } else {
    detailsBox.style.display = "block";
    detailsToggle.textContent = "بستن توضیحات";
  }
});

submitService.addEventListener("click", submitRecord);

updateSummary();
renderRecords();
renderFeedback();
</script>

</body>
</html>
```

متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم.  
همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم.
999
TEXT - 2026-05-12 00:27:46
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span> </div> </div> <div class="summary-grid"> <div class="summary-card dark"> <small>تعداد کل امروز</small> <strong id="regTotalQty">۰</strong> </div> <div class="summary-card green"> <small>جمع مبلغ امروز</small> <strong id="regTotalPrice">۰ تومان</strong> </div> </div> <div class="search-box"> <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..."> </div> <div class="section-title">خدمات پرکاربرد</div> <div class="chips" id="serviceChips"></div> <div id="selectedServiceBox" class="selected-box"> <div class="empty-box">یک خدمت را انتخاب کن</div> </div> <div class="section-title">ثبت‌های امروز</div> <div class="list-box" id="todayItems"> <div class="empty-list">هنوز چیزی ثبت نشده</div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } </style> <script> (function(){ const services = [ { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 }, { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 }, { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 }, { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 }, { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 }, { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 } ]; let selectedService = null; let currentQty = 1; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const chipsBox = document.getElementById("serviceChips"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const todayItems = document.getElementById("todayItems"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const serviceSearch = document.getElementById("serviceSearch"); const toast = document.getElementById("toast"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function getFilteredServices(){ const q = normalizeText(serviceSearch.value.trim()); if(!q) return services; return services.filter(s => normalizeText(s.name).includes(q)); } function renderChips(list = services){ chipsBox.innerHTML = ""; list.forEach(service => { const btn = document.createElement("button"); btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : ""); btn.textContent = service.name; btn.onclick = function(){ selectedService = service; currentQty = 1; renderChips(getFilteredServices()); renderSelectedService(); }; chipsBox.appendChild(btn); }); } function renderSelectedService(){ if(!selectedService){ selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`; return; } selectedServiceBox.innerHTML = ` <div class="service-card"> <div class="service-card-top"> <div> <h3>${selectedService.name}</h3> <p>قیمت واحد: ${money(selectedService.price)}</p> </div> <div class="price-badge">${money(selectedService.price * currentQty)}</div> </div> <div class="counter"> <button type="button" id="minusQty">−</button> <input type="number" id="qtyInput" min="1" value="${currentQty}"> <button type="button" id="plusQty">+</button> </div> <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button> </div> `; document.getElementById("minusQty").onclick = function(){ currentQty = Math.max(1, currentQty - 1); renderSelectedService(); }; document.getElementById("plusQty").onclick = function(){ currentQty++; renderSelectedService(); }; document.getElementById("qtyInput").oninput = function(e){ currentQty = Math.max(1, parseInt(e.target.value || "1")); renderSelectedService(); }; document.getElementById("addTodayBtn").onclick = function(){ entries.unshift({ id: Date.now(), serviceId: selectedService.id, name: selectedService.name, price: selectedService.price, qty: currentQty, status: "pending", worker: "عرفان", date: todayStr }); currentQty = 1; renderAll(); showToast("ثبت جدید اضافه شد"); }; } function renderTodayItems(){ const todayEntries = entries.filter(item => item.date === todayStr); if(todayEntries.length === 0){ todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`; return; } todayItems.innerHTML = ""; todayEntries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> `; row.querySelector(".btn-remove").onclick = function(){ entries = entries.filter(e => e.id !== item.id); renderAll(); showToast("آیتم حذف شد"); }; todayItems.appendChild(row); }); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderRegisterSummary(){ const todayEntries = entries.filter(item => item.date === todayStr); const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0); const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("regTotalQty").textContent = toFa(totalQty); document.getElementById("regTotalPrice").textContent = money(totalPrice); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmount = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmount); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderChips(getFilteredServices()); renderSelectedService(); renderTodayItems(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderRegisterSummary(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function(){ renderChips(getFilteredServices()); }); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
          </div>
        </div>

        <div class="summary-grid">
          <div class="summary-card dark">
            <small>تعداد کل امروز</small>
            <strong id="regTotalQty">۰</strong>
          </div>
          <div class="summary-card green">
            <small>جمع مبلغ امروز</small>
            <strong id="regTotalPrice">۰ تومان</strong>
          </div>
        </div>

        <div class="search-box">
          <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت...">
        </div>

        <div class="section-title">خدمات پرکاربرد</div>
        <div class="chips" id="serviceChips"></div>

        <div id="selectedServiceBox" class="selected-box">
          <div class="empty-box">یک خدمت را انتخاب کن</div>
        </div>

        <div class="section-title">ثبت‌های امروز</div>
        <div class="list-box" id="todayItems">
          <div class="empty-list">هنوز چیزی ثبت نشده</div>
        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }
</style>

<script>
(function(){
  const services = [
    { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 },
    { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 },
    { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 },
    { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 },
    { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 },
    { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 }
  ];

  let selectedService = null;
  let currentQty = 1;

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const chipsBox = document.getElementById("serviceChips");
  const selectedServiceBox = document.getElementById("selectedServiceBox");
  const todayItems = document.getElementById("todayItems");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const serviceSearch = document.getElementById("serviceSearch");
  const toast = document.getElementById("toast");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function getFilteredServices(){
    const q = normalizeText(serviceSearch.value.trim());
    if(!q) return services;
    return services.filter(s => normalizeText(s.name).includes(q));
  }

  function renderChips(list = services){
    chipsBox.innerHTML = "";
    list.forEach(service => {
      const btn = document.createElement("button");
      btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : "");
      btn.textContent = service.name;
      btn.onclick = function(){
        selectedService = service;
        currentQty = 1;
        renderChips(getFilteredServices());
        renderSelectedService();
      };
      chipsBox.appendChild(btn);
    });
  }

  function renderSelectedService(){
    if(!selectedService){
      selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`;
      return;
    }

    selectedServiceBox.innerHTML = `
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>${selectedService.name}</h3>
            <p>قیمت واحد: ${money(selectedService.price)}</p>
          </div>
          <div class="price-badge">${money(selectedService.price * currentQty)}</div>
        </div>

        <div class="counter">
          <button type="button" id="minusQty">−</button>
          <input type="number" id="qtyInput" min="1" value="${currentQty}">
          <button type="button" id="plusQty">+</button>
        </div>

        <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button>
      </div>
    `;

    document.getElementById("minusQty").onclick = function(){
      currentQty = Math.max(1, currentQty - 1);
      renderSelectedService();
    };

    document.getElementById("plusQty").onclick = function(){
      currentQty++;
      renderSelectedService();
    };

    document.getElementById("qtyInput").oninput = function(e){
      currentQty = Math.max(1, parseInt(e.target.value || "1"));
      renderSelectedService();
    };

    document.getElementById("addTodayBtn").onclick = function(){
      entries.unshift({
        id: Date.now(),
        serviceId: selectedService.id,
        name: selectedService.name,
        price: selectedService.price,
        qty: currentQty,
        status: "pending",
        worker: "عرفان",
        date: todayStr
      });
      currentQty = 1;
      renderAll();
      showToast("ثبت جدید اضافه شد");
    };
  }

  function renderTodayItems(){
    const todayEntries = entries.filter(item => item.date === todayStr);

    if(todayEntries.length === 0){
      todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`;
      return;
    }

    todayItems.innerHTML = "";
    todayEntries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      `;
      row.querySelector(".btn-remove").onclick = function(){
        entries = entries.filter(e => e.id !== item.id);
        renderAll();
        showToast("آیتم حذف شد");
      };
      todayItems.appendChild(row);
    });
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderRegisterSummary(){
    const todayEntries = entries.filter(item => item.date === todayStr);
    const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0);
    const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("regTotalQty").textContent = toFa(totalQty);
    document.getElementById("regTotalPrice").textContent = money(totalPrice);
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmount = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmount);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderChips(getFilteredServices());
    renderSelectedService();
    renderTodayItems();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderRegisterSummary();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function(){
    renderChips(getFilteredServices());
  });

  renderAll();
})();
</script>
نمونه اصلی
TEXT - 2026-05-12 00:16:52
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
۸۸۸
TEXT - 2026-05-12 00:16:47
متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم. همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم. ```html <!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ثبت کار امروز</title> <style> * { box-sizing: border-box; } body { margin: 0; padding: 14px; background: #f3f6fb; font-family: Tahoma, Arial, sans-serif; color: #111827; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-card { background: #ffffff; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: none; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width: 380px) { body { padding: 10px; } .summary-card strong { font-size: 14px; } } </style> </head> <body> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage"></small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> <script> const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; let selectedService = null; let records = []; let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || { count: 0, date: null }; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function getTodayDateKey() { const now = new Date(); return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate(); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); updatePersonalRecord(totalCount); } function updatePersonalRecord(totalCount) { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = ""; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function saveFeedback(feedback) { latestFeedback = feedback; localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback)); renderFeedback(); } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); } serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); updateSummary(); renderRecords(); renderFeedback(); </script> </body> </html> ``` متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم. همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم.
متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم.  
همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم.

```html
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ثبت کار امروز</title>

<style>
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 14px;
  background: #f3f6fb;
  font-family: Tahoma, Arial, sans-serif;
  color: #111827;
}

.worker-page {
  max-width: 520px;
  margin: 0 auto;
}

.page-header {
  margin-bottom: 14px;
}

.page-title {
  font-size: 18px;
  font-weight: 900;
  margin: 0 0 5px;
  color: #111827;
}

.page-subtitle {
  font-size: 12px;
  color: #6b7280;
  margin: 0;
  line-height: 1.8;
}

.search-card,
.form-card,
.records-card {
  background: #ffffff;
  border-radius: 20px;
  padding: 13px;
  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
  margin-bottom: 13px;
  border: 1px solid #e5e7eb;
}

.search-label {
  display: block;
  font-size: 12px;
  font-weight: 900;
  margin-bottom: 8px;
  color: #374151;
}

.search-input-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #f9fafb;
  border: 2px solid #2563eb;
  border-radius: 15px;
  padding: 10px 12px;
}

.search-icon {
  font-size: 17px;
}

#serviceSearch {
  width: 100%;
  border: none;
  outline: none;
  background: transparent;
  font-size: 14px;
  font-weight: 700;
  color: #111827;
}

#serviceSearch::placeholder {
  color: #9ca3af;
  font-weight: 500;
}

.service-results {
  margin-top: 10px;
  display: none;
}

.service-result-item {
  background: #f8fafc;
  border: 1px solid #e5e7eb;
  border-radius: 13px;
  padding: 10px;
  margin-bottom: 7px;
  cursor: pointer;
}

.service-result-item:hover {
  background: #eef2ff;
  border-color: #c7d2fe;
}

.service-result-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 3px;
}

.service-result-price {
  font-size: 11px;
  color: #6b7280;
}

.summary-wrap {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 12px;
}

.summary-card {
  background: #ffffff;
  border-radius: 17px;
  padding: 12px;
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
  border: 1px solid #e5e7eb;
}

.summary-card span {
  display: block;
  color: #6b7280;
  font-size: 11px;
  font-weight: 700;
  margin-bottom: 6px;
}

.summary-card strong {
  display: block;
  color: #111827;
  font-size: 15px;
  font-weight: 900;
}

#todayAmount {
  color: #16a34a;
}

.personal-record-card {
  background: linear-gradient(135deg, #fff7ed, #fffbeb);
  border: 1px solid #fed7aa;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  display: flex;
  align-items: center;
  gap: 11px;
  box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
}

.record-icon {
  width: 42px;
  height: 42px;
  border-radius: 14px;
  background: #ffedd5;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 21px;
  flex-shrink: 0;
}

.record-content {
  flex: 1;
}

.record-content span {
  display: block;
  color: #9a3412;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content strong {
  display: block;
  color: #111827;
  font-size: 13px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content small {
  display: none;
  color: #92400e;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.7;
}

.feedback-card {
  background: linear-gradient(135deg, #eff6ff, #f8fafc);
  border: 1px solid #bfdbfe;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
}

.feedback-title {
  font-size: 12px;
  font-weight: 900;
  color: #1d4ed8;
  margin-bottom: 7px;
}

.feedback-main {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 5px;
}

.feedback-sub {
  font-size: 11px;
  line-height: 1.8;
  color: #4b5563;
}

.feedback-badge {
  display: inline-block;
  margin-top: 8px;
  padding: 5px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 900;
}

.feedback-badge.positive {
  background: #dbeafe;
  color: #1d4ed8;
}

.feedback-badge.negative {
  background: #fef3c7;
  color: #92400e;
}

.feedback-badge.neutral {
  background: #e5e7eb;
  color: #374151;
}

.form-card {
  display: none;
}

.selected-service {
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 12px;
}

.selected-service span {
  display: block;
  color: #1d4ed8;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.selected-service strong {
  display: block;
  color: #111827;
  font-size: 14px;
  font-weight: 900;
}

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.field {
  margin-bottom: 10px;
}

.field label {
  display: block;
  font-size: 11px;
  font-weight: 900;
  color: #374151;
  margin-bottom: 6px;
}

.field input,
.field textarea {
  width: 100%;
  border: 1px solid #d1d5db;
  outline: none;
  background: #f9fafb;
  border-radius: 13px;
  padding: 10px;
  font-size: 13px;
  font-family: inherit;
}

.field input:focus,
.field textarea:focus {
  border-color: #2563eb;
  background: #ffffff;
}

.field textarea {
  min-height: 75px;
  resize: vertical;
  line-height: 1.8;
}

.details-toggle {
  width: 100%;
  border: none;
  background: #f3f4f6;
  color: #374151;
  border-radius: 13px;
  padding: 10px;
  font-size: 12px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
  margin-bottom: 10px;
}

.details-box {
  display: none;
}

.submit-btn {
  width: 100%;
  border: none;
  background: #2563eb;
  color: #ffffff;
  border-radius: 15px;
  padding: 12px;
  font-size: 14px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
}

.records-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.records-title strong {
  font-size: 14px;
  font-weight: 900;
  color: #111827;
}

.records-title span {
  font-size: 11px;
  color: #6b7280;
  font-weight: 700;
}

.empty-records {
  background: #f9fafb;
  color: #6b7280;
  text-align: center;
  border-radius: 14px;
  padding: 16px 10px;
  font-size: 12px;
  line-height: 1.8;
}

.record-item {
  border: 1px solid #e5e7eb;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 9px;
  background: #ffffff;
}

.record-item:last-child {
  margin-bottom: 0;
}

.record-top {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 7px;
}

.record-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
}

.record-time {
  font-size: 10px;
  color: #9ca3af;
  white-space: nowrap;
}

.record-info {
  font-size: 11px;
  color: #4b5563;
  line-height: 1.9;
}

.record-total {
  margin-top: 6px;
  font-size: 12px;
  font-weight: 900;
  color: #16a34a;
}

.record-desc {
  margin-top: 5px;
  color: #6b7280;
  font-size: 11px;
  line-height: 1.8;
}

@media (max-width: 380px) {
  body {
    padding: 10px;
  }

  .summary-card strong {
    font-size: 14px;
  }
}
</style>
</head>
<body>

<div class="worker-page">

  <div class="page-header">
    <h1 class="page-title">ثبت کار امروز</h1>
    <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
  </div>

  <div class="search-card">
    <label class="search-label">جستجوی خدمت</label>

    <div class="search-input-wrap">
      <div class="search-icon">🔍</div>
      <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
    </div>

    <div class="service-results" id="serviceResults"></div>
  </div>

  <div class="summary-wrap">
    <div class="summary-card">
      <span>مبلغ امروز</span>
      <strong id="todayAmount">۰ تومان</strong>
    </div>

    <div class="summary-card">
      <span>تعداد امروز</span>
      <strong id="todayCount">۰</strong>
    </div>
  </div>

  <div class="personal-record-card">
    <div class="record-icon">🏆</div>
    <div class="record-content">
      <span>رکورد روزانه تو</span>
      <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
      <small id="recordMessage"></small>
    </div>
  </div>

  <div class="feedback-card">
    <div class="feedback-title">آخرین بازخورد عملکرد</div>
    <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
    <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
    <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
  </div>

  <div class="form-card" id="serviceForm">
    <div class="selected-service">
      <span>خدمت انتخاب شده</span>
      <strong id="selectedServiceName">---</strong>
    </div>

    <div class="form-grid">
      <div class="field">
        <label>تعداد</label>
        <input type="number" id="serviceCount" min="1" value="1" />
      </div>

      <div class="field">
        <label>مقدار / مبلغ واحد</label>
        <input type="number" id="servicePrice" min="0" />
      </div>
    </div>

    <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

    <div class="details-box" id="detailsBox">
      <div class="field">
        <label>توضیحات</label>
        <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
      </div>
    </div>

    <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
  </div>

  <div class="records-card">
    <div class="records-title">
      <strong>ثبت‌های امروز</strong>
      <span id="recordsCountText">۰ مورد</span>
    </div>

    <div id="recordsList">
      <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
    </div>
  </div>

</div>

<script>
const services = [
  { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
  { name: "جوشکاری", price: 200000 },
  { name: "نجاری", price: 180000 }
];

let selectedService = null;
let records = [];

let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || {
  count: 0,
  date: null
};

let latestFeedback = {
  type: "positive",
  title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
  description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
  badge: "۸۰٪"
};

const serviceSearch = document.getElementById("serviceSearch");
const serviceResults = document.getElementById("serviceResults");
const serviceForm = document.getElementById("serviceForm");
const selectedServiceName = document.getElementById("selectedServiceName");
const serviceCount = document.getElementById("serviceCount");
const servicePrice = document.getElementById("servicePrice");
const serviceDescription = document.getElementById("serviceDescription");
const submitService = document.getElementById("submitService");
const todayAmount = document.getElementById("todayAmount");
const todayCount = document.getElementById("todayCount");
const recordsList = document.getElementById("recordsList");
const recordsCountText = document.getElementById("recordsCountText");
const detailsToggle = document.getElementById("detailsToggle");
const detailsBox = document.getElementById("detailsBox");
const bestRecordText = document.getElementById("bestRecordText");
const recordMessage = document.getElementById("recordMessage");
const feedbackMain = document.getElementById("feedbackMain");
const feedbackSub = document.getElementById("feedbackSub");
const feedbackBadge = document.getElementById("feedbackBadge");

function toPersianNumber(value) {
  return Number(value || 0).toLocaleString("fa-IR");
}

function formatToman(value) {
  return toPersianNumber(value) + " تومان";
}

function getTodayDateKey() {
  const now = new Date();
  return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate();
}

function showResults(keyword) {
  const text = keyword.trim();
  serviceResults.innerHTML = "";

  if (!text) {
    serviceResults.style.display = "none";
    return;
  }

  const filtered = services.filter(function(service) {
    return service.name.includes(text);
  });

  if (filtered.length === 0) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
      <div class="service-result-price">برای انتخاب این مورد بزنید</div>
    `;
    item.addEventListener("click", function() {
      selectService({ name: text, price: 0 });
    });
    serviceResults.appendChild(item);
    serviceResults.style.display = "block";
    return;
  }

  filtered.forEach(function(service) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">${service.name}</div>
      <div class="service-result-price">${formatToman(service.price)}</div>
    `;
    item.addEventListener("click", function() {
      selectService(service);
    });
    serviceResults.appendChild(item);
  });

  serviceResults.style.display = "block";
}

function selectService(service) {
  selectedService = service;
  selectedServiceName.textContent = service.name;
  serviceSearch.value = service.name;
  servicePrice.value = service.price || "";
  serviceCount.value = 1;
  serviceDescription.value = "";
  serviceResults.style.display = "none";
  serviceForm.style.display = "block";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";

  setTimeout(function() {
    serviceCount.focus();
  }, 100);
}

function updateSummary() {
  const totalAmount = records.reduce(function(sum, item) {
    return sum + item.total;
  }, 0);

  const totalCount = records.reduce(function(sum, item) {
    return sum + item.count;
  }, 0);

  todayAmount.textContent = formatToman(totalAmount);
  todayCount.textContent = toPersianNumber(totalCount);

  updatePersonalRecord(totalCount);
}

function updatePersonalRecord(totalCount) {
  bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
  recordMessage.textContent = "";
}

function renderRecords() {
  recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

  if (records.length === 0) {
    recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
    return;
  }

  recordsList.innerHTML = "";

  const reversed = records.slice().reverse();
  reversed.forEach(function(item) {
    const div = document.createElement("div");
    div.className = "record-item";
    div.innerHTML = `
      <div class="record-top">
        <div class="record-name">${item.name}</div>
        <div class="record-time">${item.time}</div>
      </div>
      <div class="record-info">
        تعداد: ${toPersianNumber(item.count)} |
        مبلغ واحد: ${formatToman(item.price)}
      </div>
      <div class="record-total">
        جمع: ${formatToman(item.total)}
      </div>
      ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
    `;
    recordsList.appendChild(div);
  });
}

function renderFeedback() {
  feedbackMain.textContent = latestFeedback.title;
  feedbackSub.textContent = latestFeedback.description;
  feedbackBadge.textContent = latestFeedback.badge;
  feedbackBadge.className = "feedback-badge " + latestFeedback.type;
}

function saveFeedback(feedback) {
  latestFeedback = feedback;
  localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback));
  renderFeedback();
}

function submitRecord() {
  if (!selectedService) {
    alert("اول یک خدمت را انتخاب کن.");
    return;
  }

  const count = parseInt(serviceCount.value, 10);
  const price = parseInt(servicePrice.value, 10);
  const description = serviceDescription.value.trim();

  if (!count || count <= 0) {
    alert("تعداد را درست وارد کن.");
    return;
  }

  if (isNaN(price) || price < 0) {
    alert("مبلغ را درست وارد کن.");
    return;
  }

  const total = count * price;
  const now = new Date();

  records.push({
    name: selectedService.name,
    count: count,
    price: price,
    total: total,
    description: description,
    time: now.toLocaleTimeString("fa-IR", {
      hour: "2-digit",
      minute: "2-digit"
    })
  });

  renderRecords();
  updateSummary();

  selectedService = null;
  serviceSearch.value = "";
  serviceCount.value = 1;
  servicePrice.value = "";
  serviceDescription.value = "";
  selectedServiceName.textContent = "---";
  serviceForm.style.display = "none";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";
  serviceSearch.focus();
}

serviceSearch.addEventListener("input", function() {
  showResults(serviceSearch.value);
});

detailsToggle.addEventListener("click", function() {
  if (detailsBox.style.display === "block") {
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
  } else {
    detailsBox.style.display = "block";
    detailsToggle.textContent = "بستن توضیحات";
  }
});

submitService.addEventListener("click", submitRecord);

updateSummary();
renderRecords();
renderFeedback();
</script>

</body>
</html>
```

متعهد می‌شوم که کد پایه را تغییر ندهم و فقط تغییراتی که شما بگویید را اعمال کنم.  
همچنین هیچ‌وقت کد را تکه‌تکه نفرستم و همیشه کد را یک‌جا و کامل ارائه بدهم.
نمونه اصلی
TEXT - 2026-05-12 00:12:31
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
۷۷۷
TEXT - 2026-05-12 00:12:24
<!doctype html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>داشبورد کارگاهی</title> <style> :root{ --bg:#f6f7fb; --card:#ffffff; --text:#1f2937; --muted:#6b7280; --line:#e5e7eb; --accent:#2563eb; --good:#16a34a; --bad:#dc2626; --warn:#d97706; --shadow:0 10px 30px rgba(0,0,0,.06); --radius:18px; } *{box-sizing:border-box} body{ margin:0; font-family:Tahoma, sans-serif; background:var(--bg); color:var(--text); } .wrap{ max-width:1100px; margin:24px auto; padding:0 14px 40px; } .topbar{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:18px; } .title{ font-size:22px; font-weight:700; } .datebox{ color:var(--muted); font-size:14px; } .grid{ display:grid; grid-template-columns:repeat(12,1fr); gap:14px; } .card{ grid-column:span 12; background:var(--card); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:16px; } @media(min-width:768px){ .span-4{grid-column:span 4} .span-6{grid-column:span 6} .span-8{grid-column:span 8} } .card h3{ margin:0 0 12px; font-size:16px; } .stat{ display:flex; align-items:center; justify-content:space-between; gap:10px; } .stat .value{ font-size:28px; font-weight:800; line-height:1.2; } .stat .sub{ color:var(--muted); font-size:13px; margin-top:6px; } .pill{ padding:7px 10px; border-radius:999px; font-size:12px; font-weight:700; white-space:nowrap; } .pill.good{background:#dcfce7;color:var(--good)} .pill.bad{background:#fee2e2;color:var(--bad)} .pill.warn{background:#ffedd5;color:var(--warn)} .today-amount{ color:var(--good); } .services{ display:grid; gap:10px; margin-top:8px; } .service{ display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px; border:1px solid var(--line); border-radius:14px; background:#fafafa; } .service .name{ font-weight:600; font-size:14px; } .service .price{ color:var(--accent); font-weight:800; font-size:14px; white-space:nowrap; } .feedback{ display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } .feedback-main{ font-size:18px; font-weight:800; margin-bottom:8px; } .feedback-sub{ color:var(--muted); font-size:14px; line-height:1.8; } .feedback-badge{ min-width:74px; text-align:center; padding:10px 12px; border-radius:14px; font-weight:800; font-size:18px; } .feedback-badge.positive{ background:#dcfce7; color:var(--good); } .feedback-badge.negative{ background:#fee2e2; color:var(--bad); } .record-box{ display:flex; flex-direction:column; gap:8px; } .record-value{ font-size:24px; font-weight:800; } .record-date{ color:var(--muted); font-size:13px; } #recordMessage{ display:none; } </style> </head> <body> <div class="wrap"> <div class="topbar"> <div class="title">داشبورد کارگاهی</div> <div class="datebox" id="todayDate"></div> </div> <div class="grid"> <section class="card span-4"> <h3>درآمد امروز</h3> <div class="stat"> <div> <div class="value today-amount" id="todayAmount">۰ تومان</div> <div class="sub">جمع کل ثبت‌شده امروز</div> </div> <div class="pill good">امروز</div> </div> </section> <section class="card span-4"> <h3>آخرین بازخورد عملکرد</h3> <div class="feedback"> <div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div> </div> <div class="feedback-badge negative" id="feedbackBadge">۸۰٪</div> </div> </section> <section class="card span-4"> <h3>رکورد روزانه</h3> <div class="record-box"> <div class="record-value" id="recordValue">۲,۰۰۰,۰۰۰ تومان</div> <div class="record-date" id="recordDate">در ۱۴۰۵/۰۱/۲۱</div> <div id="recordMessage">این متن به صورت ثابت نمایش داده می‌شود</div> </div> </section> <section class="card span-8"> <h3>خدمات / مواد اولیه</h3> <div class="services" id="servicesList"></div> </section> </div> </div> <script> function toPersianDigits(input){ return input.toString().replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function formatToman(num){ return toPersianDigits(Number(num).toLocaleString("en-US")) + " تومان"; } const today = new Date(); const todayText = today.toLocaleDateString("fa-IR"); document.getElementById("todayDate").textContent = "تاریخ امروز: " + todayText; const todayIncome = 850000; document.getElementById("todayAmount").textContent = formatToman(todayIncome); let latestFeedback = { type: "negative", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز", badge: "۸۰٪" }; document.getElementById("feedbackMain").textContent = latestFeedback.title; document.getElementById("feedbackSub").textContent = latestFeedback.description; const feedbackBadge = document.getElementById("feedbackBadge"); feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.classList.remove("positive", "negative"); feedbackBadge.classList.add(latestFeedback.type === "positive" ? "positive" : "negative"); const recordValue = 2000000; const recordDate = "۱۴۰۵/۰۱/۲۱"; document.getElementById("recordValue").textContent = formatToman(recordValue); document.getElementById("recordDate").textContent = "در " + recordDate; const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 }, { name: "جوشکاری", price: 200000 }, { name: "نجاری", price: 180000 } ]; const servicesList = document.getElementById("servicesList"); services.forEach(service => { const item = document.createElement("div"); item.className = "service"; const name = document.createElement("div"); name.className = "name"; name.textContent = service.name; const price = document.createElement("div"); price.className = "price"; price.textContent = formatToman(service.price); item.appendChild(name); item.appendChild(price); servicesList.appendChild(item); }); </script> </body> </html>
<!doctype html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>داشبورد کارگاهی</title>
  <style>
    :root{
      --bg:#f6f7fb;
      --card:#ffffff;
      --text:#1f2937;
      --muted:#6b7280;
      --line:#e5e7eb;
      --accent:#2563eb;
      --good:#16a34a;
      --bad:#dc2626;
      --warn:#d97706;
      --shadow:0 10px 30px rgba(0,0,0,.06);
      --radius:18px;
    }
    *{box-sizing:border-box}
    body{
      margin:0;
      font-family:Tahoma, sans-serif;
      background:var(--bg);
      color:var(--text);
    }
    .wrap{
      max-width:1100px;
      margin:24px auto;
      padding:0 14px 40px;
    }
    .topbar{
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:12px;
      margin-bottom:18px;
    }
    .title{
      font-size:22px;
      font-weight:700;
    }
    .datebox{
      color:var(--muted);
      font-size:14px;
    }
    .grid{
      display:grid;
      grid-template-columns:repeat(12,1fr);
      gap:14px;
    }
    .card{
      grid-column:span 12;
      background:var(--card);
      border:1px solid var(--line);
      border-radius:var(--radius);
      box-shadow:var(--shadow);
      padding:16px;
    }
    @media(min-width:768px){
      .span-4{grid-column:span 4}
      .span-6{grid-column:span 6}
      .span-8{grid-column:span 8}
    }
    .card h3{
      margin:0 0 12px;
      font-size:16px;
    }
    .stat{
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:10px;
    }
    .stat .value{
      font-size:28px;
      font-weight:800;
      line-height:1.2;
    }
    .stat .sub{
      color:var(--muted);
      font-size:13px;
      margin-top:6px;
    }
    .pill{
      padding:7px 10px;
      border-radius:999px;
      font-size:12px;
      font-weight:700;
      white-space:nowrap;
    }
    .pill.good{background:#dcfce7;color:var(--good)}
    .pill.bad{background:#fee2e2;color:var(--bad)}
    .pill.warn{background:#ffedd5;color:var(--warn)}
    .today-amount{
      color:var(--good);
    }
    .services{
      display:grid;
      gap:10px;
      margin-top:8px;
    }
    .service{
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:12px;
      padding:12px;
      border:1px solid var(--line);
      border-radius:14px;
      background:#fafafa;
    }
    .service .name{
      font-weight:600;
      font-size:14px;
    }
    .service .price{
      color:var(--accent);
      font-weight:800;
      font-size:14px;
      white-space:nowrap;
    }
    .feedback{
      display:flex;
      align-items:flex-start;
      justify-content:space-between;
      gap:12px;
    }
    .feedback-main{
      font-size:18px;
      font-weight:800;
      margin-bottom:8px;
    }
    .feedback-sub{
      color:var(--muted);
      font-size:14px;
      line-height:1.8;
    }
    .feedback-badge{
      min-width:74px;
      text-align:center;
      padding:10px 12px;
      border-radius:14px;
      font-weight:800;
      font-size:18px;
    }
    .feedback-badge.positive{
      background:#dcfce7;
      color:var(--good);
    }
    .feedback-badge.negative{
      background:#fee2e2;
      color:var(--bad);
    }
    .record-box{
      display:flex;
      flex-direction:column;
      gap:8px;
    }
    .record-value{
      font-size:24px;
      font-weight:800;
    }
    .record-date{
      color:var(--muted);
      font-size:13px;
    }
    #recordMessage{
      display:none;
    }
  </style>
</head>
<body>
  <div class="wrap">
    <div class="topbar">
      <div class="title">داشبورد کارگاهی</div>
      <div class="datebox" id="todayDate"></div>
    </div>

    <div class="grid">
      <section class="card span-4">
        <h3>درآمد امروز</h3>
        <div class="stat">
          <div>
            <div class="value today-amount" id="todayAmount">۰ تومان</div>
            <div class="sub">جمع کل ثبت‌شده امروز</div>
          </div>
          <div class="pill good">امروز</div>
        </div>
      </section>

      <section class="card span-4">
        <h3>آخرین بازخورد عملکرد</h3>
        <div class="feedback">
          <div>
            <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
            <div class="feedback-sub" id="feedbackSub">آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز</div>
          </div>
          <div class="feedback-badge negative" id="feedbackBadge">۸۰٪</div>
        </div>
      </section>

      <section class="card span-4">
        <h3>رکورد روزانه</h3>
        <div class="record-box">
          <div class="record-value" id="recordValue">۲,۰۰۰,۰۰۰ تومان</div>
          <div class="record-date" id="recordDate">در ۱۴۰۵/۰۱/۲۱</div>
          <div id="recordMessage">این متن به صورت ثابت نمایش داده می‌شود</div>
        </div>
      </section>

      <section class="card span-8">
        <h3>خدمات / مواد اولیه</h3>
        <div class="services" id="servicesList"></div>
      </section>
    </div>
  </div>

  <script>
    function toPersianDigits(input){
      return input.toString().replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]);
    }

    function formatToman(num){
      return toPersianDigits(Number(num).toLocaleString("en-US")) + " تومان";
    }

    const today = new Date();
    const todayText = today.toLocaleDateString("fa-IR");
    document.getElementById("todayDate").textContent = "تاریخ امروز: " + todayText;

    const todayIncome = 850000;
    document.getElementById("todayAmount").textContent = formatToman(todayIncome);

    let latestFeedback = {
      type: "negative",
      title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
      description: "آخرین دلیل کسر امتیاز: اشتباه رنگ کردن میز",
      badge: "۸۰٪"
    };

    document.getElementById("feedbackMain").textContent = latestFeedback.title;
    document.getElementById("feedbackSub").textContent = latestFeedback.description;

    const feedbackBadge = document.getElementById("feedbackBadge");
    feedbackBadge.textContent = latestFeedback.badge;
    feedbackBadge.classList.remove("positive", "negative");
    feedbackBadge.classList.add(latestFeedback.type === "positive" ? "positive" : "negative");

    const recordValue = 2000000;
    const recordDate = "۱۴۰۵/۰۱/۲۱";
    document.getElementById("recordValue").textContent = formatToman(recordValue);
    document.getElementById("recordDate").textContent = "در " + recordDate;

    const services = [
      { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 150000 },
      { name: "جوشکاری", price: 200000 },
      { name: "نجاری", price: 180000 }
    ];

    const servicesList = document.getElementById("servicesList");
    services.forEach(service => {
      const item = document.createElement("div");
      item.className = "service";

      const name = document.createElement("div");
      name.className = "name";
      name.textContent = service.name;

      const price = document.createElement("div");
      price.className = "price";
      price.textContent = formatToman(service.price);

      item.appendChild(name);
      item.appendChild(price);
      servicesList.appendChild(item);
    });
  </script>
</body>
</html>
نمونه اصلی
TEXT - 2026-05-11 23:52:03
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
ثثثث
TEXT - 2026-05-11 23:51:56
<!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ثبت کار امروز</title> <style> * { box-sizing: border-box; } body { margin: 0; padding: 14px; background: #f3f6fb; font-family: Tahoma, Arial, sans-serif; color: #111827; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-card { background: #ffffff; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } #todayAmount { color: #16a34a; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: block; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dbeafe; color: #1d4ed8; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width: 380px) { body { padding: 10px; } .summary-card strong { font-size: 14px; } } </style> </head> <body> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong> <small id="recordMessage">این متن به صورت ثابت نمایش داده می‌شود.</small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div> <div class="feedback-sub" id="feedbackSub">سطح عملکرد فعلی به صورت امتیازی نمایش داده می‌شود.</div> <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> <script> const services = [ { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 0 }, { name: "جوشکاری", price: 0 }, { name: "نجاری", price: 0 } ]; let selectedService = null; let records = []; let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || { count: 0, date: null }; let latestFeedback = { type: "positive", title: "امتیاز عملکرد: ۸۰ از ۱۰۰", description: "سطح عملکرد فعلی به صورت امتیازی نمایش داده می‌شود.", badge: "۸۰٪" }; const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function getTodayDateKey() { const now = new Date(); return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate(); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); updatePersonalRecord(totalCount); } function updatePersonalRecord(totalCount) { bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱"; recordMessage.textContent = "این متن به صورت ثابت نمایش داده می‌شود."; } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function saveFeedback(feedback) { latestFeedback = feedback; localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback)); renderFeedback(); } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); } serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); updateSummary(); renderRecords(); renderFeedback(); </script> </body> </html>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ثبت کار امروز</title>

<style>
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 14px;
  background: #f3f6fb;
  font-family: Tahoma, Arial, sans-serif;
  color: #111827;
}

.worker-page {
  max-width: 520px;
  margin: 0 auto;
}

.page-header {
  margin-bottom: 14px;
}

.page-title {
  font-size: 18px;
  font-weight: 900;
  margin: 0 0 5px;
  color: #111827;
}

.page-subtitle {
  font-size: 12px;
  color: #6b7280;
  margin: 0;
  line-height: 1.8;
}

.search-card,
.form-card,
.records-card {
  background: #ffffff;
  border-radius: 20px;
  padding: 13px;
  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
  margin-bottom: 13px;
  border: 1px solid #e5e7eb;
}

.search-label {
  display: block;
  font-size: 12px;
  font-weight: 900;
  margin-bottom: 8px;
  color: #374151;
}

.search-input-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #f9fafb;
  border: 2px solid #2563eb;
  border-radius: 15px;
  padding: 10px 12px;
}

.search-icon {
  font-size: 17px;
}

#serviceSearch {
  width: 100%;
  border: none;
  outline: none;
  background: transparent;
  font-size: 14px;
  font-weight: 700;
  color: #111827;
}

#serviceSearch::placeholder {
  color: #9ca3af;
  font-weight: 500;
}

.service-results {
  margin-top: 10px;
  display: none;
}

.service-result-item {
  background: #f8fafc;
  border: 1px solid #e5e7eb;
  border-radius: 13px;
  padding: 10px;
  margin-bottom: 7px;
  cursor: pointer;
}

.service-result-item:hover {
  background: #eef2ff;
  border-color: #c7d2fe;
}

.service-result-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 3px;
}

.service-result-price {
  font-size: 11px;
  color: #6b7280;
}

.summary-wrap {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 12px;
}

.summary-card {
  background: #ffffff;
  border-radius: 17px;
  padding: 12px;
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
  border: 1px solid #e5e7eb;
}

.summary-card span {
  display: block;
  color: #6b7280;
  font-size: 11px;
  font-weight: 700;
  margin-bottom: 6px;
}

.summary-card strong {
  display: block;
  color: #111827;
  font-size: 15px;
  font-weight: 900;
}

#todayAmount {
  color: #16a34a;
}

.personal-record-card {
  background: linear-gradient(135deg, #fff7ed, #fffbeb);
  border: 1px solid #fed7aa;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  display: flex;
  align-items: center;
  gap: 11px;
  box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
}

.record-icon {
  width: 42px;
  height: 42px;
  border-radius: 14px;
  background: #ffedd5;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 21px;
  flex-shrink: 0;
}

.record-content {
  flex: 1;
}

.record-content span {
  display: block;
  color: #9a3412;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content strong {
  display: block;
  color: #111827;
  font-size: 13px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content small {
  display: block;
  color: #92400e;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.7;
}

.feedback-card {
  background: linear-gradient(135deg, #eff6ff, #f8fafc);
  border: 1px solid #bfdbfe;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
}

.feedback-title {
  font-size: 12px;
  font-weight: 900;
  color: #1d4ed8;
  margin-bottom: 7px;
}

.feedback-main {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 5px;
}

.feedback-sub {
  font-size: 11px;
  line-height: 1.8;
  color: #4b5563;
}

.feedback-badge {
  display: inline-block;
  margin-top: 8px;
  padding: 5px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 900;
}

.feedback-badge.positive {
  background: #dbeafe;
  color: #1d4ed8;
}

.feedback-badge.negative {
  background: #fef3c7;
  color: #92400e;
}

.feedback-badge.neutral {
  background: #e5e7eb;
  color: #374151;
}

.form-card {
  display: none;
}

.selected-service {
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 12px;
}

.selected-service span {
  display: block;
  color: #1d4ed8;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.selected-service strong {
  display: block;
  color: #111827;
  font-size: 14px;
  font-weight: 900;
}

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.field {
  margin-bottom: 10px;
}

.field label {
  display: block;
  font-size: 11px;
  font-weight: 900;
  color: #374151;
  margin-bottom: 6px;
}

.field input,
.field textarea {
  width: 100%;
  border: 1px solid #d1d5db;
  outline: none;
  background: #f9fafb;
  border-radius: 13px;
  padding: 10px;
  font-size: 13px;
  font-family: inherit;
}

.field input:focus,
.field textarea:focus {
  border-color: #2563eb;
  background: #ffffff;
}

.field textarea {
  min-height: 75px;
  resize: vertical;
  line-height: 1.8;
}

.details-toggle {
  width: 100%;
  border: none;
  background: #f3f4f6;
  color: #374151;
  border-radius: 13px;
  padding: 10px;
  font-size: 12px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
  margin-bottom: 10px;
}

.details-box {
  display: none;
}

.submit-btn {
  width: 100%;
  border: none;
  background: #2563eb;
  color: #ffffff;
  border-radius: 15px;
  padding: 12px;
  font-size: 14px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
}

.records-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.records-title strong {
  font-size: 14px;
  font-weight: 900;
  color: #111827;
}

.records-title span {
  font-size: 11px;
  color: #6b7280;
  font-weight: 700;
}

.empty-records {
  background: #f9fafb;
  color: #6b7280;
  text-align: center;
  border-radius: 14px;
  padding: 16px 10px;
  font-size: 12px;
  line-height: 1.8;
}

.record-item {
  border: 1px solid #e5e7eb;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 9px;
  background: #ffffff;
}

.record-item:last-child {
  margin-bottom: 0;
}

.record-top {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 7px;
}

.record-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
}

.record-time {
  font-size: 10px;
  color: #9ca3af;
  white-space: nowrap;
}

.record-info {
  font-size: 11px;
  color: #4b5563;
  line-height: 1.9;
}

.record-total {
  margin-top: 6px;
  font-size: 12px;
  font-weight: 900;
  color: #16a34a;
}

.record-desc {
  margin-top: 5px;
  color: #6b7280;
  font-size: 11px;
  line-height: 1.8;
}

@media (max-width: 380px) {
  body {
    padding: 10px;
  }

  .summary-card strong {
    font-size: 14px;
  }
}
</style>
</head>
<body>

<div class="worker-page">

  <div class="page-header">
    <h1 class="page-title">ثبت کار امروز</h1>
    <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
  </div>

  <div class="search-card">
    <label class="search-label">جستجوی خدمت</label>

    <div class="search-input-wrap">
      <div class="search-icon">🔍</div>
      <input type="text" id="serviceSearch" placeholder="مثلاً رنگ میز، جوشکاری، نجاری..." />
    </div>

    <div class="service-results" id="serviceResults"></div>
  </div>

  <div class="summary-wrap">
    <div class="summary-card">
      <span>مبلغ امروز</span>
      <strong id="todayAmount">۰ تومان</strong>
    </div>

    <div class="summary-card">
      <span>تعداد امروز</span>
      <strong id="todayCount">۰</strong>
    </div>
  </div>

  <div class="personal-record-card">
    <div class="record-icon">🏆</div>
    <div class="record-content">
      <span>رکورد روزانه تو</span>
      <strong id="bestRecordText">۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱</strong>
      <small id="recordMessage">این متن به صورت ثابت نمایش داده می‌شود.</small>
    </div>
  </div>

  <div class="feedback-card">
    <div class="feedback-title">آخرین بازخورد عملکرد</div>
    <div class="feedback-main" id="feedbackMain">امتیاز عملکرد: ۸۰ از ۱۰۰</div>
    <div class="feedback-sub" id="feedbackSub">سطح عملکرد فعلی به صورت امتیازی نمایش داده می‌شود.</div>
    <div class="feedback-badge positive" id="feedbackBadge">۸۰٪</div>
  </div>

  <div class="form-card" id="serviceForm">
    <div class="selected-service">
      <span>خدمت انتخاب شده</span>
      <strong id="selectedServiceName">---</strong>
    </div>

    <div class="form-grid">
      <div class="field">
        <label>تعداد</label>
        <input type="number" id="serviceCount" min="1" value="1" />
      </div>

      <div class="field">
        <label>مقدار / مبلغ واحد</label>
        <input type="number" id="servicePrice" min="0" />
      </div>
    </div>

    <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

    <div class="details-box" id="detailsBox">
      <div class="field">
        <label>توضیحات</label>
        <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
      </div>
    </div>

    <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
  </div>

  <div class="records-card">
    <div class="records-title">
      <strong>ثبت‌های امروز</strong>
      <span id="recordsCountText">۰ مورد</span>
    </div>

    <div id="recordsList">
      <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
    </div>
  </div>

</div>

<script>
const services = [
  { name: "رنگ میز لبه دار از ۳۵ تا ۱۰۰ سانت", price: 0 },
  { name: "جوشکاری", price: 0 },
  { name: "نجاری", price: 0 }
];

let selectedService = null;
let records = [];

let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || {
  count: 0,
  date: null
};

let latestFeedback = {
  type: "positive",
  title: "امتیاز عملکرد: ۸۰ از ۱۰۰",
  description: "سطح عملکرد فعلی به صورت امتیازی نمایش داده می‌شود.",
  badge: "۸۰٪"
};

const serviceSearch = document.getElementById("serviceSearch");
const serviceResults = document.getElementById("serviceResults");
const serviceForm = document.getElementById("serviceForm");
const selectedServiceName = document.getElementById("selectedServiceName");
const serviceCount = document.getElementById("serviceCount");
const servicePrice = document.getElementById("servicePrice");
const serviceDescription = document.getElementById("serviceDescription");
const submitService = document.getElementById("submitService");
const todayAmount = document.getElementById("todayAmount");
const todayCount = document.getElementById("todayCount");
const recordsList = document.getElementById("recordsList");
const recordsCountText = document.getElementById("recordsCountText");
const detailsToggle = document.getElementById("detailsToggle");
const detailsBox = document.getElementById("detailsBox");
const bestRecordText = document.getElementById("bestRecordText");
const recordMessage = document.getElementById("recordMessage");
const feedbackMain = document.getElementById("feedbackMain");
const feedbackSub = document.getElementById("feedbackSub");
const feedbackBadge = document.getElementById("feedbackBadge");

function toPersianNumber(value) {
  return Number(value || 0).toLocaleString("fa-IR");
}

function formatToman(value) {
  return toPersianNumber(value) + " تومان";
}

function getTodayDateKey() {
  const now = new Date();
  return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate();
}

function showResults(keyword) {
  const text = keyword.trim();
  serviceResults.innerHTML = "";

  if (!text) {
    serviceResults.style.display = "none";
    return;
  }

  const filtered = services.filter(function(service) {
    return service.name.includes(text);
  });

  if (filtered.length === 0) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
      <div class="service-result-price">برای انتخاب این مورد بزنید</div>
    `;
    item.addEventListener("click", function() {
      selectService({ name: text, price: 0 });
    });
    serviceResults.appendChild(item);
    serviceResults.style.display = "block";
    return;
  }

  filtered.forEach(function(service) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">${service.name}</div>
      <div class="service-result-price">${formatToman(service.price)}</div>
    `;
    item.addEventListener("click", function() {
      selectService(service);
    });
    serviceResults.appendChild(item);
  });

  serviceResults.style.display = "block";
}

function selectService(service) {
  selectedService = service;
  selectedServiceName.textContent = service.name;
  serviceSearch.value = service.name;
  servicePrice.value = service.price || "";
  serviceCount.value = 1;
  serviceDescription.value = "";
  serviceResults.style.display = "none";
  serviceForm.style.display = "block";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";

  setTimeout(function() {
    serviceCount.focus();
  }, 100);
}

function updateSummary() {
  const totalAmount = records.reduce(function(sum, item) {
    return sum + item.total;
  }, 0);

  const totalCount = records.reduce(function(sum, item) {
    return sum + item.count;
  }, 0);

  todayAmount.textContent = formatToman(totalAmount);
  todayCount.textContent = toPersianNumber(totalCount);

  updatePersonalRecord(totalCount);
}

function updatePersonalRecord(totalCount) {
  bestRecordText.textContent = "۲,۰۰۰,۰۰۰ تومان در ۱۴۰۵/۰۱/۲۱";
  recordMessage.textContent = "این متن به صورت ثابت نمایش داده می‌شود.";
}

function renderRecords() {
  recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

  if (records.length === 0) {
    recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
    return;
  }

  recordsList.innerHTML = "";

  const reversed = records.slice().reverse();
  reversed.forEach(function(item) {
    const div = document.createElement("div");
    div.className = "record-item";
    div.innerHTML = `
      <div class="record-top">
        <div class="record-name">${item.name}</div>
        <div class="record-time">${item.time}</div>
      </div>
      <div class="record-info">
        تعداد: ${toPersianNumber(item.count)} |
        مبلغ واحد: ${formatToman(item.price)}
      </div>
      <div class="record-total">
        جمع: ${formatToman(item.total)}
      </div>
      ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
    `;
    recordsList.appendChild(div);
  });
}

function renderFeedback() {
  feedbackMain.textContent = latestFeedback.title;
  feedbackSub.textContent = latestFeedback.description;
  feedbackBadge.textContent = latestFeedback.badge;
  feedbackBadge.className = "feedback-badge " + latestFeedback.type;
}

function saveFeedback(feedback) {
  latestFeedback = feedback;
  localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback));
  renderFeedback();
}

function submitRecord() {
  if (!selectedService) {
    alert("اول یک خدمت را انتخاب کن.");
    return;
  }

  const count = parseInt(serviceCount.value, 10);
  const price = parseInt(servicePrice.value, 10);
  const description = serviceDescription.value.trim();

  if (!count || count <= 0) {
    alert("تعداد را درست وارد کن.");
    return;
  }

  if (isNaN(price) || price < 0) {
    alert("مبلغ را درست وارد کن.");
    return;
  }

  const total = count * price;
  const now = new Date();

  records.push({
    name: selectedService.name,
    count: count,
    price: price,
    total: total,
    description: description,
    time: now.toLocaleTimeString("fa-IR", {
      hour: "2-digit",
      minute: "2-digit"
    })
  });

  renderRecords();
  updateSummary();

  selectedService = null;
  serviceSearch.value = "";
  serviceCount.value = 1;
  servicePrice.value = "";
  serviceDescription.value = "";
  selectedServiceName.textContent = "---";
  serviceForm.style.display = "none";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";
  serviceSearch.focus();
}

serviceSearch.addEventListener("input", function() {
  showResults(serviceSearch.value);
});

detailsToggle.addEventListener("click", function() {
  if (detailsBox.style.display === "block") {
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
  } else {
    detailsBox.style.display = "block";
    detailsToggle.textContent = "بستن توضیحات";
  }
});

submitService.addEventListener("click", submitRecord);

updateSummary();
renderRecords();
renderFeedback();
</script>

</body>
</html>
نمونه اصلی
TEXT - 2026-05-11 23:48:15
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
غغغ
TEXT - 2026-05-11 23:48:02
<!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ثبت سریع کار امروز</title> <style> * { box-sizing: border-box; } body { margin: 0; font-family: Vazirmatn, Tahoma, sans-serif; background: #f3f5f7; color: #1f2937; } .container { max-width: 720px; margin: 0 auto; padding: 16px; } .page-title { font-size: 22px; font-weight: 800; margin: 6px 0 18px; color: #111827; } .top-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; } .card { background: #fff; border-radius: 16px; padding: 14px; box-shadow: 0 6px 20px rgba(0,0,0,.05); border: 1px solid #e5e7eb; } .mini-card .label { font-size: 12px; color: #6b7280; margin-bottom: 8px; } .mini-card .value { font-size: 19px; font-weight: 800; line-height: 1.4; color: #111827; } .mini-card .sub { margin-top: 6px; font-size: 11px; color: #9ca3af; } .record-title { font-size: 13px; font-weight: 700; color: #374151; margin-bottom: 10px; } .record-main { font-size: 16px; font-weight: 800; color: #111827; margin-bottom: 4px; } .record-sub { font-size: 12px; color: #6b7280; } .search-card { margin-bottom: 14px; } .search-label { display: block; font-size: 13px; font-weight: 700; color: #374151; margin-bottom: 8px; } .search-wrap { position: relative; } .search-input { width: 100%; padding: 13px 14px; border-radius: 12px; border: 1px solid #d1d5db; font-size: 14px; outline: none; background: #fff; } .search-input:focus { border-color: #2563eb; box-shadow: 0 0 0 4px rgba(37,99,235,.08); } .service-list { margin-top: 10px; display: grid; gap: 8px; } .service-item { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 12px; padding: 12px 14px; cursor: pointer; transition: .15s ease; } .service-item:hover { background: #eff6ff; border-color: #bfdbfe; } .service-name { font-size: 14px; font-weight: 700; color: #111827; } .service-price { margin-top: 4px; font-size: 12px; color: #6b7280; } .empty-search { font-size: 12px; color: #9ca3af; padding: 8px 2px 2px; } .form-card { display: none; margin-bottom: 14px; } .form-card.active { display: block; } .selected-service { margin-bottom: 12px; background: #eff6ff; border: 1px solid #bfdbfe; color: #1d4ed8; padding: 10px 12px; border-radius: 12px; font-size: 13px; font-weight: 700; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 8px; } .field label { display: block; font-size: 12px; color: #374151; margin-bottom: 6px; font-weight: 700; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; border-radius: 12px; padding: 12px; font-family: inherit; font-size: 14px; outline: none; background: #fff; } .field input:focus, .field textarea:focus { border-color: #2563eb; box-shadow: 0 0 0 4px rgba(37,99,235,.08); } .details-toggle { margin: 4px 0 10px; background: transparent; border: none; color: #2563eb; font-size: 13px; font-weight: 700; cursor: pointer; padding: 0; } .details-box { display: none; margin-bottom: 12px; } .details-box.open { display: block; } .submit-btn { width: 100%; border: none; background: #2563eb; color: white; border-radius: 12px; padding: 13px 16px; font-size: 14px; font-weight: 800; cursor: pointer; transition: .15s ease; } .submit-btn:hover { background: #1d4ed8; } .today-list-title { font-size: 15px; font-weight: 800; color: #111827; margin: 18px 0 10px; } .today-list { display: grid; gap: 10px; } .today-item { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 13px 14px; box-shadow: 0 4px 14px rgba(0,0,0,.04); } .today-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; } .today-service { font-size: 14px; font-weight: 800; color: #111827; } .today-total { font-size: 14px; font-weight: 900; color: #111827; white-space: nowrap; } .today-meta { display: flex; flex-wrap: wrap; gap: 10px; font-size: 12px; color: #6b7280; margin-bottom: 8px; } .today-desc { font-size: 12px; color: #4b5563; line-height: 1.8; background: #f9fafb; border-radius: 10px; padding: 10px; border: 1px dashed #e5e7eb; } .muted { color: #9ca3af; font-size: 12px; text-align: center; padding: 18px 10px; background: #fff; border: 1px dashed #d1d5db; border-radius: 12px; } @media (max-width: 640px) { .top-grid { grid-template-columns: 1fr; } .form-grid { grid-template-columns: 1fr; } .today-top { flex-direction: column; align-items: flex-start; } } </style> </head> <body> <div class="container"> <div class="page-title">ثبت سریع کار امروز</div> <div class="top-grid"> <div class="card mini-card"> <div class="label">مبلغ امروز</div> <div class="value" id="todayAmount">۰ تومان</div> <div class="sub">جمع ثبت‌های امروز</div> </div> <div class="card mini-card"> <div class="label">تعداد امروز</div> <div class="value" id="todayCount">۰</div> <div class="sub">تعداد خدمات ثبت‌شده</div> </div> <div class="card"> <div class="record-title">رکورد روزانه تو</div> <div class="record-main" id="recordMain">هنوز رکوردی ثبت نشده</div> <div class="record-sub" id="recordSub">با ثبت کارهای امروز، رکوردت اینجا نمایش داده می‌شود</div> </div> </div> <div class="card search-card"> <label class="search-label" for="serviceSearch">جستجوی خدمت</label> <div class="search-wrap"> <input id="serviceSearch" class="search-input" type="text" placeholder="مثلاً تعمیر" /> </div> <div class="service-list" id="serviceList"></div> </div> <div class="card form-card" id="formCard"> <div class="selected-service" id="selectedServiceBox">خدمت انتخاب نشده است</div> <div class="form-grid"> <div class="field"> <label for="countInput">تعداد</label> <input id="countInput" type="number" min="1" value="1" /> </div> <div class="field"> <label for="amountInput">مبلغ (تومان)</label> <input id="amountInput" type="number" min="0" placeholder="مثلاً 250000" /> </div> </div> <button class="details-toggle" id="detailsToggle" type="button">افزودن توضیحات</button> <div class="details-box" id="detailsBox"> <div class="field"> <label for="descInput">توضیحات</label> <textarea id="descInput" rows="3" placeholder="اگر توضیحی لازم است اینجا بنویس"></textarea> </div> </div> <button class="submit-btn" id="submitBtn" type="button">ثبت خدمت</button> </div> <div class="today-list-title">ثبت‌های امروز</div> <div class="today-list" id="todayList"></div> </div> <script> const services = [ { id: 1, name: "شستشو", price: 120000 }, { id: 2, name: "نصب", price: 250000 }, { id: 3, name: "تعمیر", price: 180000 } ]; let selectedService = null; let todayRecords = JSON.parse(localStorage.getItem("todayRecords")) || []; let personalRecord = JSON.parse(localStorage.getItem("personalRecord")) || null; const serviceSearch = document.getElementById("serviceSearch"); const serviceList = document.getElementById("serviceList"); const formCard = document.getElementById("formCard"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const countInput = document.getElementById("countInput"); const amountInput = document.getElementById("amountInput"); const descInput = document.getElementById("descInput"); const submitBtn = document.getElementById("submitBtn"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const todayList = document.getElementById("todayList"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const recordMain = document.getElementById("recordMain"); const recordSub = document.getElementById("recordSub"); function toPersianNumber(value) { return String(value).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function formatPrice(num) { return toPersianNumber(Number(num || 0).toLocaleString("en-US")) + " تومان"; } function escapeHtml(text) { return text .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function getTodayLabel() { const now = new Date(); const y = now.getFullYear(); const m = String(now.getMonth() + 1).padStart(2, "0"); const d = String(now.getDate()).padStart(2, "0"); return `${y}/${m}/${d}`; } function renderServices(list) { if (!list.length) { serviceList.innerHTML = `<div class="empty-search">خدمتی پیدا نشد</div>`; return; } serviceList.innerHTML = list.map(service => ` <div class="service-item" data-id="${service.id}"> <div class="service-name">${service.name}</div> <div class="service-price">مبلغ پیشنهادی: ${formatPrice(service.price)}</div> </div> `).join(""); document.querySelectorAll(".service-item").forEach(item => { item.addEventListener("click", () => { const id = Number(item.dataset.id); const service = services.find(s => s.id === id); selectedService = service; selectedServiceBox.textContent = `خدمت انتخاب‌شده: ${service.name}`; amountInput.value = service.price; countInput.value = 1; descInput.value = ""; formCard.classList.add("active"); serviceSearch.value = service.name; serviceList.innerHTML = ""; }); }); } function updatePersonalRecord(totalAmountToday) { if (!personalRecord || totalAmountToday > personalRecord.amount) { personalRecord = { amount: totalAmountToday, date: getTodayLabel() }; localStorage.setItem("personalRecord", JSON.stringify(personalRecord)); } if (personalRecord) { recordMain.textContent = formatPrice(personalRecord.amount); recordSub.textContent = `ثبت‌شده در ${toPersianNumber(personalRecord.date)}`; } else { recordMain.textContent = "هنوز رکوردی ثبت نشده"; recordSub.textContent = "با ثبت کارهای امروز، رکوردت اینجا نمایش داده می‌شود"; } } function updateSummary() { const totalAmount = todayRecords.reduce((sum, item) => sum + item.total, 0); const totalCount = todayRecords.reduce((sum, item) => sum + item.count, 0); todayAmount.textContent = formatPrice(totalAmount); todayCount.textContent = toPersianNumber(totalCount); updatePersonalRecord(totalAmount); } function renderTodayList() { if (!todayRecords.length) { todayList.innerHTML = `<div class="muted">هنوز هیچ خدمتی برای امروز ثبت نشده است</div>`; return; } todayList.innerHTML = [...todayRecords].reverse().map(item => ` <div class="today-item"> <div class="today-top"> <div class="today-service">${item.serviceName}</div> <div class="today-total">${formatPrice(item.total)}</div> </div> <div class="today-meta"> <div>تعداد: ${toPersianNumber(item.count)}</div> <div>مبلغ واحد: ${formatPrice(item.unitPrice)}</div> </div> ${item.description ? `<div class="today-desc">${escapeHtml(item.description)}</div>` : ""} </div> `).join(""); } function saveRecords() { localStorage.setItem("todayRecords", JSON.stringify(todayRecords)); } serviceSearch.addEventListener("input", () => { const q = serviceSearch.value.trim(); if (!q) { renderServices(services); return; } const filtered = services.filter(service => service.name.includes(q)); renderServices(filtered); }); detailsToggle.addEventListener("click", () => { detailsBox.classList.toggle("open"); detailsToggle.textContent = detailsBox.classList.contains("open") ? "بستن توضیحات" : "افزودن توضیحات"; }); submitBtn.addEventListener("click", () => { if (!selectedService) { alert("لطفاً ابتدا یک خدمت انتخاب کنید."); return; } const count = Number(countInput.value); const unitPrice = Number(amountInput.value); const description = descInput.value.trim(); if (!count || count < 1) { alert("تعداد معتبر وارد کنید."); return; } if (isNaN(unitPrice) || unitPrice < 0) { alert("مبلغ معتبر وارد کنید."); return; } const record = { serviceId: selectedService.id, serviceName: selectedService.name, count, unitPrice, total: count * unitPrice, description }; todayRecords.push(record); saveRecords(); updateSummary(); renderTodayList(); selectedService = null; formCard.classList.remove("active"); selectedServiceBox.textContent = "خدمت انتخاب نشده است"; serviceSearch.value = ""; countInput.value = 1; amountInput.value = ""; descInput.value = ""; detailsBox.classList.remove("open"); detailsToggle.textContent = "افزودن توضیحات"; renderServices(services); }); renderServices(services); updateSummary(); renderTodayList(); </script> </body> </html>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>ثبت سریع کار امروز</title>
  <style>
    * { box-sizing: border-box; }

    body {
      margin: 0;
      font-family: Vazirmatn, Tahoma, sans-serif;
      background: #f3f5f7;
      color: #1f2937;
    }

    .container {
      max-width: 720px;
      margin: 0 auto;
      padding: 16px;
    }

    .page-title {
      font-size: 22px;
      font-weight: 800;
      margin: 6px 0 18px;
      color: #111827;
    }

    .top-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 12px;
      margin-bottom: 16px;
    }

    .card {
      background: #fff;
      border-radius: 16px;
      padding: 14px;
      box-shadow: 0 6px 20px rgba(0,0,0,.05);
      border: 1px solid #e5e7eb;
    }

    .mini-card .label {
      font-size: 12px;
      color: #6b7280;
      margin-bottom: 8px;
    }

    .mini-card .value {
      font-size: 19px;
      font-weight: 800;
      line-height: 1.4;
      color: #111827;
    }

    .mini-card .sub {
      margin-top: 6px;
      font-size: 11px;
      color: #9ca3af;
    }

    .record-title {
      font-size: 13px;
      font-weight: 700;
      color: #374151;
      margin-bottom: 10px;
    }

    .record-main {
      font-size: 16px;
      font-weight: 800;
      color: #111827;
      margin-bottom: 4px;
    }

    .record-sub {
      font-size: 12px;
      color: #6b7280;
    }

    .search-card {
      margin-bottom: 14px;
    }

    .search-label {
      display: block;
      font-size: 13px;
      font-weight: 700;
      color: #374151;
      margin-bottom: 8px;
    }

    .search-wrap {
      position: relative;
    }

    .search-input {
      width: 100%;
      padding: 13px 14px;
      border-radius: 12px;
      border: 1px solid #d1d5db;
      font-size: 14px;
      outline: none;
      background: #fff;
    }

    .search-input:focus {
      border-color: #2563eb;
      box-shadow: 0 0 0 4px rgba(37,99,235,.08);
    }

    .service-list {
      margin-top: 10px;
      display: grid;
      gap: 8px;
    }

    .service-item {
      background: #f9fafb;
      border: 1px solid #e5e7eb;
      border-radius: 12px;
      padding: 12px 14px;
      cursor: pointer;
      transition: .15s ease;
    }

    .service-item:hover {
      background: #eff6ff;
      border-color: #bfdbfe;
    }

    .service-name {
      font-size: 14px;
      font-weight: 700;
      color: #111827;
    }

    .service-price {
      margin-top: 4px;
      font-size: 12px;
      color: #6b7280;
    }

    .empty-search {
      font-size: 12px;
      color: #9ca3af;
      padding: 8px 2px 2px;
    }

    .form-card {
      display: none;
      margin-bottom: 14px;
    }

    .form-card.active {
      display: block;
    }

    .selected-service {
      margin-bottom: 12px;
      background: #eff6ff;
      border: 1px solid #bfdbfe;
      color: #1d4ed8;
      padding: 10px 12px;
      border-radius: 12px;
      font-size: 13px;
      font-weight: 700;
    }

    .form-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 12px;
      margin-bottom: 8px;
    }

    .field label {
      display: block;
      font-size: 12px;
      color: #374151;
      margin-bottom: 6px;
      font-weight: 700;
    }

    .field input,
    .field textarea {
      width: 100%;
      border: 1px solid #d1d5db;
      border-radius: 12px;
      padding: 12px;
      font-family: inherit;
      font-size: 14px;
      outline: none;
      background: #fff;
    }

    .field input:focus,
    .field textarea:focus {
      border-color: #2563eb;
      box-shadow: 0 0 0 4px rgba(37,99,235,.08);
    }

    .details-toggle {
      margin: 4px 0 10px;
      background: transparent;
      border: none;
      color: #2563eb;
      font-size: 13px;
      font-weight: 700;
      cursor: pointer;
      padding: 0;
    }

    .details-box {
      display: none;
      margin-bottom: 12px;
    }

    .details-box.open {
      display: block;
    }

    .submit-btn {
      width: 100%;
      border: none;
      background: #2563eb;
      color: white;
      border-radius: 12px;
      padding: 13px 16px;
      font-size: 14px;
      font-weight: 800;
      cursor: pointer;
      transition: .15s ease;
    }

    .submit-btn:hover {
      background: #1d4ed8;
    }

    .today-list-title {
      font-size: 15px;
      font-weight: 800;
      color: #111827;
      margin: 18px 0 10px;
    }

    .today-list {
      display: grid;
      gap: 10px;
    }

    .today-item {
      background: #fff;
      border: 1px solid #e5e7eb;
      border-radius: 14px;
      padding: 13px 14px;
      box-shadow: 0 4px 14px rgba(0,0,0,.04);
    }

    .today-top {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      margin-bottom: 8px;
    }

    .today-service {
      font-size: 14px;
      font-weight: 800;
      color: #111827;
    }

    .today-total {
      font-size: 14px;
      font-weight: 900;
      color: #111827;
      white-space: nowrap;
    }

    .today-meta {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      font-size: 12px;
      color: #6b7280;
      margin-bottom: 8px;
    }

    .today-desc {
      font-size: 12px;
      color: #4b5563;
      line-height: 1.8;
      background: #f9fafb;
      border-radius: 10px;
      padding: 10px;
      border: 1px dashed #e5e7eb;
    }

    .muted {
      color: #9ca3af;
      font-size: 12px;
      text-align: center;
      padding: 18px 10px;
      background: #fff;
      border: 1px dashed #d1d5db;
      border-radius: 12px;
    }

    @media (max-width: 640px) {
      .top-grid {
        grid-template-columns: 1fr;
      }

      .form-grid {
        grid-template-columns: 1fr;
      }

      .today-top {
        flex-direction: column;
        align-items: flex-start;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="page-title">ثبت سریع کار امروز</div>

    <div class="top-grid">
      <div class="card mini-card">
        <div class="label">مبلغ امروز</div>
        <div class="value" id="todayAmount">۰ تومان</div>
        <div class="sub">جمع ثبت‌های امروز</div>
      </div>

      <div class="card mini-card">
        <div class="label">تعداد امروز</div>
        <div class="value" id="todayCount">۰</div>
        <div class="sub">تعداد خدمات ثبت‌شده</div>
      </div>

      <div class="card">
        <div class="record-title">رکورد روزانه تو</div>
        <div class="record-main" id="recordMain">هنوز رکوردی ثبت نشده</div>
        <div class="record-sub" id="recordSub">با ثبت کارهای امروز، رکوردت اینجا نمایش داده می‌شود</div>
      </div>
    </div>

    <div class="card search-card">
      <label class="search-label" for="serviceSearch">جستجوی خدمت</label>
      <div class="search-wrap">
        <input
          id="serviceSearch"
          class="search-input"
          type="text"
          placeholder="مثلاً تعمیر"
        />
      </div>
      <div class="service-list" id="serviceList"></div>
    </div>

    <div class="card form-card" id="formCard">
      <div class="selected-service" id="selectedServiceBox">خدمت انتخاب نشده است</div>

      <div class="form-grid">
        <div class="field">
          <label for="countInput">تعداد</label>
          <input id="countInput" type="number" min="1" value="1" />
        </div>

        <div class="field">
          <label for="amountInput">مبلغ (تومان)</label>
          <input id="amountInput" type="number" min="0" placeholder="مثلاً 250000" />
        </div>
      </div>

      <button class="details-toggle" id="detailsToggle" type="button">افزودن توضیحات</button>

      <div class="details-box" id="detailsBox">
        <div class="field">
          <label for="descInput">توضیحات</label>
          <textarea id="descInput" rows="3" placeholder="اگر توضیحی لازم است اینجا بنویس"></textarea>
        </div>
      </div>

      <button class="submit-btn" id="submitBtn" type="button">ثبت خدمت</button>
    </div>

    <div class="today-list-title">ثبت‌های امروز</div>
    <div class="today-list" id="todayList"></div>
  </div>

  <script>
    const services = [
      { id: 1, name: "شستشو", price: 120000 },
      { id: 2, name: "نصب", price: 250000 },
      { id: 3, name: "تعمیر", price: 180000 }
    ];

    let selectedService = null;
    let todayRecords = JSON.parse(localStorage.getItem("todayRecords")) || [];
    let personalRecord = JSON.parse(localStorage.getItem("personalRecord")) || null;

    const serviceSearch = document.getElementById("serviceSearch");
    const serviceList = document.getElementById("serviceList");
    const formCard = document.getElementById("formCard");
    const selectedServiceBox = document.getElementById("selectedServiceBox");
    const countInput = document.getElementById("countInput");
    const amountInput = document.getElementById("amountInput");
    const descInput = document.getElementById("descInput");
    const submitBtn = document.getElementById("submitBtn");
    const todayAmount = document.getElementById("todayAmount");
    const todayCount = document.getElementById("todayCount");
    const todayList = document.getElementById("todayList");
    const detailsToggle = document.getElementById("detailsToggle");
    const detailsBox = document.getElementById("detailsBox");
    const recordMain = document.getElementById("recordMain");
    const recordSub = document.getElementById("recordSub");

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

    function formatPrice(num) {
      return toPersianNumber(Number(num || 0).toLocaleString("en-US")) + " تومان";
    }

    function escapeHtml(text) {
      return text
        .replaceAll("&", "&")
        .replaceAll("<", "<")
        .replaceAll(">", ">")
        .replaceAll('"', """)
        .replaceAll("'", "'");
    }

    function getTodayLabel() {
      const now = new Date();
      const y = now.getFullYear();
      const m = String(now.getMonth() + 1).padStart(2, "0");
      const d = String(now.getDate()).padStart(2, "0");
      return `${y}/${m}/${d}`;
    }

    function renderServices(list) {
      if (!list.length) {
        serviceList.innerHTML = `<div class="empty-search">خدمتی پیدا نشد</div>`;
        return;
      }

      serviceList.innerHTML = list.map(service => `
        <div class="service-item" data-id="${service.id}">
          <div class="service-name">${service.name}</div>
          <div class="service-price">مبلغ پیشنهادی: ${formatPrice(service.price)}</div>
        </div>
      `).join("");

      document.querySelectorAll(".service-item").forEach(item => {
        item.addEventListener("click", () => {
          const id = Number(item.dataset.id);
          const service = services.find(s => s.id === id);
          selectedService = service;

          selectedServiceBox.textContent = `خدمت انتخاب‌شده: ${service.name}`;
          amountInput.value = service.price;
          countInput.value = 1;
          descInput.value = "";
          formCard.classList.add("active");
          serviceSearch.value = service.name;
          serviceList.innerHTML = "";
        });
      });
    }

    function updatePersonalRecord(totalAmountToday) {
      if (!personalRecord || totalAmountToday > personalRecord.amount) {
        personalRecord = {
          amount: totalAmountToday,
          date: getTodayLabel()
        };
        localStorage.setItem("personalRecord", JSON.stringify(personalRecord));
      }

      if (personalRecord) {
        recordMain.textContent = formatPrice(personalRecord.amount);
        recordSub.textContent = `ثبت‌شده در ${toPersianNumber(personalRecord.date)}`;
      } else {
        recordMain.textContent = "هنوز رکوردی ثبت نشده";
        recordSub.textContent = "با ثبت کارهای امروز، رکوردت اینجا نمایش داده می‌شود";
      }
    }

    function updateSummary() {
      const totalAmount = todayRecords.reduce((sum, item) => sum + item.total, 0);
      const totalCount = todayRecords.reduce((sum, item) => sum + item.count, 0);

      todayAmount.textContent = formatPrice(totalAmount);
      todayCount.textContent = toPersianNumber(totalCount);
      updatePersonalRecord(totalAmount);
    }

    function renderTodayList() {
      if (!todayRecords.length) {
        todayList.innerHTML = `<div class="muted">هنوز هیچ خدمتی برای امروز ثبت نشده است</div>`;
        return;
      }

      todayList.innerHTML = [...todayRecords].reverse().map(item => `
        <div class="today-item">
          <div class="today-top">
            <div class="today-service">${item.serviceName}</div>
            <div class="today-total">${formatPrice(item.total)}</div>
          </div>
          <div class="today-meta">
            <div>تعداد: ${toPersianNumber(item.count)}</div>
            <div>مبلغ واحد: ${formatPrice(item.unitPrice)}</div>
          </div>
          ${item.description ? `<div class="today-desc">${escapeHtml(item.description)}</div>` : ""}
        </div>
      `).join("");
    }

    function saveRecords() {
      localStorage.setItem("todayRecords", JSON.stringify(todayRecords));
    }

    serviceSearch.addEventListener("input", () => {
      const q = serviceSearch.value.trim();
      if (!q) {
        renderServices(services);
        return;
      }

      const filtered = services.filter(service => service.name.includes(q));
      renderServices(filtered);
    });

    detailsToggle.addEventListener("click", () => {
      detailsBox.classList.toggle("open");
      detailsToggle.textContent = detailsBox.classList.contains("open")
        ? "بستن توضیحات"
        : "افزودن توضیحات";
    });

    submitBtn.addEventListener("click", () => {
      if (!selectedService) {
        alert("لطفاً ابتدا یک خدمت انتخاب کنید.");
        return;
      }

      const count = Number(countInput.value);
      const unitPrice = Number(amountInput.value);
      const description = descInput.value.trim();

      if (!count || count < 1) {
        alert("تعداد معتبر وارد کنید.");
        return;
      }

      if (isNaN(unitPrice) || unitPrice < 0) {
        alert("مبلغ معتبر وارد کنید.");
        return;
      }

      const record = {
        serviceId: selectedService.id,
        serviceName: selectedService.name,
        count,
        unitPrice,
        total: count * unitPrice,
        description
      };

      todayRecords.push(record);
      saveRecords();
      updateSummary();
      renderTodayList();

      selectedService = null;
      formCard.classList.remove("active");
      selectedServiceBox.textContent = "خدمت انتخاب نشده است";
      serviceSearch.value = "";
      countInput.value = 1;
      amountInput.value = "";
      descInput.value = "";
      detailsBox.classList.remove("open");
      detailsToggle.textContent = "افزودن توضیحات";
      renderServices(services);
    });

    renderServices(services);
    updateSummary();
    renderTodayList();
  </script>
</body>
</html>
نمونه اصلی
TEXT - 2026-05-11 23:45:33
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
پپپپ
TEXT - 2026-05-11 23:45:27
<!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ثبت سریع کار امروز</title> <style> * { box-sizing: border-box; } body { margin: 0; font-family: Vazirmatn, Tahoma, sans-serif; background: #f3f5f7; color: #1f2937; } .container { max-width: 720px; margin: 0 auto; padding: 16px; } .page-title { font-size: 22px; font-weight: 800; margin: 6px 0 18px; color: #111827; } .top-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; } .card { background: #fff; border-radius: 16px; padding: 14px; box-shadow: 0 6px 20px rgba(0,0,0,.05); border: 1px solid #e5e7eb; } .mini-card .label { font-size: 12px; color: #6b7280; margin-bottom: 8px; } .mini-card .value { font-size: 19px; font-weight: 800; line-height: 1.4; color: #111827; } .mini-card.money .value { color: #16a34a; } .mini-card .sub { margin-top: 6px; font-size: 11px; color: #9ca3af; } .record-title, .score-title { font-size: 13px; font-weight: 700; color: #374151; margin-bottom: 10px; } .record-amount { font-size: 20px; font-weight: 900; color: #111827; margin-bottom: 4px; } .record-date { font-size: 12px; color: #6b7280; } .score-box { display: flex; align-items: baseline; gap: 6px; } .score-main { font-size: 26px; font-weight: 900; color: #2563eb; line-height: 1; } .score-total { font-size: 14px; font-weight: 700; color: #6b7280; } .score-caption { margin-top: 8px; font-size: 11px; color: #9ca3af; } .search-card { margin-bottom: 14px; } .search-label { display: block; font-size: 13px; font-weight: 700; color: #374151; margin-bottom: 8px; } .search-wrap { position: relative; } .search-input { width: 100%; padding: 13px 14px; border-radius: 12px; border: 1px solid #d1d5db; font-size: 14px; outline: none; background: #fff; } .search-input:focus { border-color: #2563eb; box-shadow: 0 0 0 4px rgba(37,99,235,.08); } .service-list { margin-top: 10px; display: grid; gap: 8px; } .service-item { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 12px; padding: 12px 14px; cursor: pointer; transition: .15s ease; } .service-item:hover { background: #eff6ff; border-color: #bfdbfe; } .service-name { font-size: 14px; font-weight: 700; color: #111827; } .service-price { margin-top: 4px; font-size: 12px; color: #6b7280; } .empty-search { font-size: 12px; color: #9ca3af; padding: 8px 2px 2px; } .form-card { display: none; margin-bottom: 14px; } .form-card.active { display: block; } .selected-service { margin-bottom: 12px; background: #eff6ff; border: 1px solid #bfdbfe; color: #1d4ed8; padding: 10px 12px; border-radius: 12px; font-size: 13px; font-weight: 700; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; } .field label { display: block; font-size: 12px; color: #374151; margin-bottom: 6px; font-weight: 700; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; border-radius: 12px; padding: 12px; font-family: inherit; font-size: 14px; outline: none; background: #fff; } .field input:focus, .field textarea:focus { border-color: #2563eb; box-shadow: 0 0 0 4px rgba(37,99,235,.08); } .details-toggle { margin: 4px 0 10px; background: transparent; border: none; color: #2563eb; font-size: 13px; font-weight: 700; cursor: pointer; padding: 0; } .details-box { display: none; margin-bottom: 12px; } .details-box.open { display: block; } .submit-btn { width: 100%; border: none; background: #2563eb; color: white; border-radius: 12px; padding: 13px 16px; font-size: 14px; font-weight: 800; cursor: pointer; transition: .15s ease; } .submit-btn:hover { background: #1d4ed8; } .today-list-title { font-size: 15px; font-weight: 800; color: #111827; margin: 18px 0 10px; } .today-list { display: grid; gap: 10px; } .today-item { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 13px 14px; box-shadow: 0 4px 14px rgba(0,0,0,.04); } .today-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; } .today-service { font-size: 14px; font-weight: 800; color: #111827; } .today-total { font-size: 14px; font-weight: 900; color: #16a34a; white-space: nowrap; } .today-meta { display: flex; flex-wrap: wrap; gap: 10px; font-size: 12px; color: #6b7280; margin-bottom: 8px; } .today-desc { font-size: 12px; color: #4b5563; line-height: 1.8; background: #f9fafb; border-radius: 10px; padding: 10px; border: 1px dashed #e5e7eb; } .muted { color: #9ca3af; font-size: 12px; text-align: center; padding: 18px 10px; background: #fff; border: 1px dashed #d1d5db; border-radius: 12px; } @media (max-width: 640px) { .top-grid { grid-template-columns: 1fr; } .form-grid { grid-template-columns: 1fr; } .today-top { flex-direction: column; align-items: flex-start; } } </style> </head> <body> <div class="container"> <div class="page-title">ثبت سریع کار امروز</div> <div class="top-grid"> <div class="card mini-card money"> <div class="label">مبلغ امروز</div> <div class="value" id="todayAmount">۰ تومان</div> <div class="sub">جمع ثبت‌های امروز</div> </div> <div class="card mini-card"> <div class="label">تعداد امروز</div> <div class="value" id="todayCount">۰</div> <div class="sub">تعداد خدمات ثبت‌شده</div> </div> <div class="card"> <div class="score-title">امتیاز عملکرد</div> <div class="score-box"> <div class="score-main" id="performanceScore">۸۰</div> <div class="score-total">از ۱۰۰</div> </div> <div class="score-caption">نمایش خلاصه و ساده عملکرد</div> </div> <div class="card" style="grid-column: 1 / -1;"> <div class="record-title">رکورد روزانه</div> <div class="record-amount" id="recordAmount">۲,۰۰۰,۰۰۰ تومان</div> <div class="record-date" id="recordDate">ثبت‌شده در ۱۴۰۵/۰۱/۲۱</div> </div> </div> <div class="card search-card"> <label class="search-label" for="serviceSearch">جستجوی خدمت</label> <div class="search-wrap"> <input id="serviceSearch" class="search-input" type="text" placeholder="مثلاً جوشکاری" /> </div> <div class="service-list" id="serviceList"></div> </div> <div class="card form-card" id="formCard"> <div class="selected-service" id="selectedServiceBox">خدمت انتخاب نشده است</div> <div class="form-grid"> <div class="field"> <label for="countInput">تعداد</label> <input id="countInput" type="number" min="1" value="1" /> </div> <div class="field"> <label for="amountInput">مبلغ (تومان)</label> <input id="amountInput" type="number" min="0" placeholder="مثلاً 250000" /> </div> </div> <button class="details-toggle" id="detailsToggle" type="button">افزودن توضیحات</button> <div class="details-box" id="detailsBox"> <div class="field"> <label for="descInput">توضیحات</label> <textarea id="descInput" rows="3" placeholder="اگر توضیحی لازم است اینجا بنویس"></textarea> </div> </div> <button class="submit-btn" id="submitBtn" type="button">ثبت خدمت</button> </div> <div class="today-list-title">ثبت‌های امروز</div> <div class="today-list" id="todayList"></div> </div> <script> const services = [ { id: 1, name: "رنگ میز لبه‌دار ۳۵ تا ۱۰۰ سانت", price: 350000 }, { id: 2, name: "جوشکاری", price: 450000 }, { id: 3, name: "نجاری", price: 300000 } ]; let selectedService = null; let todayRecords = JSON.parse(localStorage.getItem("todayRecords")) || []; const serviceSearch = document.getElementById("serviceSearch"); const serviceList = document.getElementById("serviceList"); const formCard = document.getElementById("formCard"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const countInput = document.getElementById("countInput"); const amountInput = document.getElementById("amountInput"); const descInput = document.getElementById("descInput"); const submitBtn = document.getElementById("submitBtn"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const todayList = document.getElementById("todayList"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); function toPersianNumber(value) { return String(value).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function formatPrice(num) { return toPersianNumber(Number(num || 0).toLocaleString("en-US")) + " تومان"; } function escapeHtml(text) { return text .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function renderServices(list) { if (!list.length) { serviceList.innerHTML = `<div class="empty-search">خدمتی پیدا نشد</div>`; return; } serviceList.innerHTML = list.map(service => ` <div class="service-item" data-id="${service.id}"> <div class="service-name">${service.name}</div> <div class="service-price">مبلغ پیشنهادی: ${formatPrice(service.price)}</div> </div> `).join(""); document.querySelectorAll(".service-item").forEach(item => { item.addEventListener("click", () => { const id = Number(item.dataset.id); const service = services.find(s => s.id === id); selectedService = service; selectedServiceBox.textContent = `خدمت انتخاب‌شده: ${service.name}`; amountInput.value = service.price; countInput.value = 1; descInput.value = ""; formCard.classList.add("active"); serviceSearch.value = service.name; serviceList.innerHTML = ""; }); }); } function updateSummary() { const totalAmount = todayRecords.reduce((sum, item) => sum + item.total, 0); const totalCount = todayRecords.reduce((sum, item) => sum + item.count, 0); todayAmount.textContent = formatPrice(totalAmount); todayCount.textContent = toPersianNumber(totalCount); } function renderTodayList() { if (!todayRecords.length) { todayList.innerHTML = `<div class="muted">هنوز هیچ خدمتی برای امروز ثبت نشده است</div>`; return; } todayList.innerHTML = [...todayRecords].reverse().map(item => ` <div class="today-item"> <div class="today-top"> <div class="today-service">${item.serviceName}</div> <div class="today-total">${formatPrice(item.total)}</div> </div> <div class="today-meta"> <div>تعداد: ${toPersianNumber(item.count)}</div> <div>مبلغ واحد: ${formatPrice(item.unitPrice)}</div> </div> ${item.description ? `<div class="today-desc">${escapeHtml(item.description)}</div>` : ""} </div> `).join(""); } function saveRecords() { localStorage.setItem("todayRecords", JSON.stringify(todayRecords)); } serviceSearch.addEventListener("input", () => { const q = serviceSearch.value.trim(); if (!q) { renderServices(services); return; } const filtered = services.filter(service => service.name.includes(q)); renderServices(filtered); }); detailsToggle.addEventListener("click", () => { detailsBox.classList.toggle("open"); detailsToggle.textContent = detailsBox.classList.contains("open") ? "بستن توضیحات" : "افزودن توضیحات"; }); submitBtn.addEventListener("click", () => { if (!selectedService) { alert("لطفاً ابتدا یک خدمت انتخاب کنید."); return; } const count = Number(countInput.value); const unitPrice = Number(amountInput.value); const description = descInput.value.trim(); if (!count || count < 1) { alert("تعداد معتبر وارد کنید."); return; } if (isNaN(unitPrice) || unitPrice < 0) { alert("مبلغ معتبر وارد کنید."); return; } const record = { serviceId: selectedService.id, serviceName: selectedService.name, count, unitPrice, total: count * unitPrice, description }; todayRecords.push(record); saveRecords(); updateSummary(); renderTodayList(); selectedService = null; formCard.classList.remove("active"); selectedServiceBox.textContent = "خدمت انتخاب نشده است"; serviceSearch.value = ""; countInput.value = 1; amountInput.value = ""; descInput.value = ""; detailsBox.classList.remove("open"); detailsToggle.textContent = "افزودن توضیحات"; renderServices(services); }); renderServices(services); updateSummary(); renderTodayList(); </script> </body> </html>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>ثبت سریع کار امروز</title>
  <style>
    * { box-sizing: border-box; }

    body {
      margin: 0;
      font-family: Vazirmatn, Tahoma, sans-serif;
      background: #f3f5f7;
      color: #1f2937;
    }

    .container {
      max-width: 720px;
      margin: 0 auto;
      padding: 16px;
    }

    .page-title {
      font-size: 22px;
      font-weight: 800;
      margin: 6px 0 18px;
      color: #111827;
    }

    .top-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 12px;
      margin-bottom: 16px;
    }

    .card {
      background: #fff;
      border-radius: 16px;
      padding: 14px;
      box-shadow: 0 6px 20px rgba(0,0,0,.05);
      border: 1px solid #e5e7eb;
    }

    .mini-card .label {
      font-size: 12px;
      color: #6b7280;
      margin-bottom: 8px;
    }

    .mini-card .value {
      font-size: 19px;
      font-weight: 800;
      line-height: 1.4;
      color: #111827;
    }

    .mini-card.money .value {
      color: #16a34a;
    }

    .mini-card .sub {
      margin-top: 6px;
      font-size: 11px;
      color: #9ca3af;
    }

    .record-title,
    .score-title {
      font-size: 13px;
      font-weight: 700;
      color: #374151;
      margin-bottom: 10px;
    }

    .record-amount {
      font-size: 20px;
      font-weight: 900;
      color: #111827;
      margin-bottom: 4px;
    }

    .record-date {
      font-size: 12px;
      color: #6b7280;
    }

    .score-box {
      display: flex;
      align-items: baseline;
      gap: 6px;
    }

    .score-main {
      font-size: 26px;
      font-weight: 900;
      color: #2563eb;
      line-height: 1;
    }

    .score-total {
      font-size: 14px;
      font-weight: 700;
      color: #6b7280;
    }

    .score-caption {
      margin-top: 8px;
      font-size: 11px;
      color: #9ca3af;
    }

    .search-card {
      margin-bottom: 14px;
    }

    .search-label {
      display: block;
      font-size: 13px;
      font-weight: 700;
      color: #374151;
      margin-bottom: 8px;
    }

    .search-wrap {
      position: relative;
    }

    .search-input {
      width: 100%;
      padding: 13px 14px;
      border-radius: 12px;
      border: 1px solid #d1d5db;
      font-size: 14px;
      outline: none;
      background: #fff;
    }

    .search-input:focus {
      border-color: #2563eb;
      box-shadow: 0 0 0 4px rgba(37,99,235,.08);
    }

    .service-list {
      margin-top: 10px;
      display: grid;
      gap: 8px;
    }

    .service-item {
      background: #f9fafb;
      border: 1px solid #e5e7eb;
      border-radius: 12px;
      padding: 12px 14px;
      cursor: pointer;
      transition: .15s ease;
    }

    .service-item:hover {
      background: #eff6ff;
      border-color: #bfdbfe;
    }

    .service-name {
      font-size: 14px;
      font-weight: 700;
      color: #111827;
    }

    .service-price {
      margin-top: 4px;
      font-size: 12px;
      color: #6b7280;
    }

    .empty-search {
      font-size: 12px;
      color: #9ca3af;
      padding: 8px 2px 2px;
    }

    .form-card {
      display: none;
      margin-bottom: 14px;
    }

    .form-card.active {
      display: block;
    }

    .selected-service {
      margin-bottom: 12px;
      background: #eff6ff;
      border: 1px solid #bfdbfe;
      color: #1d4ed8;
      padding: 10px 12px;
      border-radius: 12px;
      font-size: 13px;
      font-weight: 700;
    }

    .form-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 12px;
      margin-bottom: 12px;
    }

    .field label {
      display: block;
      font-size: 12px;
      color: #374151;
      margin-bottom: 6px;
      font-weight: 700;
    }

    .field input,
    .field textarea {
      width: 100%;
      border: 1px solid #d1d5db;
      border-radius: 12px;
      padding: 12px;
      font-family: inherit;
      font-size: 14px;
      outline: none;
      background: #fff;
    }

    .field input:focus,
    .field textarea:focus {
      border-color: #2563eb;
      box-shadow: 0 0 0 4px rgba(37,99,235,.08);
    }

    .details-toggle {
      margin: 4px 0 10px;
      background: transparent;
      border: none;
      color: #2563eb;
      font-size: 13px;
      font-weight: 700;
      cursor: pointer;
      padding: 0;
    }

    .details-box {
      display: none;
      margin-bottom: 12px;
    }

    .details-box.open {
      display: block;
    }

    .submit-btn {
      width: 100%;
      border: none;
      background: #2563eb;
      color: white;
      border-radius: 12px;
      padding: 13px 16px;
      font-size: 14px;
      font-weight: 800;
      cursor: pointer;
      transition: .15s ease;
    }

    .submit-btn:hover {
      background: #1d4ed8;
    }

    .today-list-title {
      font-size: 15px;
      font-weight: 800;
      color: #111827;
      margin: 18px 0 10px;
    }

    .today-list {
      display: grid;
      gap: 10px;
    }

    .today-item {
      background: #fff;
      border: 1px solid #e5e7eb;
      border-radius: 14px;
      padding: 13px 14px;
      box-shadow: 0 4px 14px rgba(0,0,0,.04);
    }

    .today-top {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      margin-bottom: 8px;
    }

    .today-service {
      font-size: 14px;
      font-weight: 800;
      color: #111827;
    }

    .today-total {
      font-size: 14px;
      font-weight: 900;
      color: #16a34a;
      white-space: nowrap;
    }

    .today-meta {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      font-size: 12px;
      color: #6b7280;
      margin-bottom: 8px;
    }

    .today-desc {
      font-size: 12px;
      color: #4b5563;
      line-height: 1.8;
      background: #f9fafb;
      border-radius: 10px;
      padding: 10px;
      border: 1px dashed #e5e7eb;
    }

    .muted {
      color: #9ca3af;
      font-size: 12px;
      text-align: center;
      padding: 18px 10px;
      background: #fff;
      border: 1px dashed #d1d5db;
      border-radius: 12px;
    }

    @media (max-width: 640px) {
      .top-grid {
        grid-template-columns: 1fr;
      }

      .form-grid {
        grid-template-columns: 1fr;
      }

      .today-top {
        flex-direction: column;
        align-items: flex-start;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="page-title">ثبت سریع کار امروز</div>

    <div class="top-grid">
      <div class="card mini-card money">
        <div class="label">مبلغ امروز</div>
        <div class="value" id="todayAmount">۰ تومان</div>
        <div class="sub">جمع ثبت‌های امروز</div>
      </div>

      <div class="card mini-card">
        <div class="label">تعداد امروز</div>
        <div class="value" id="todayCount">۰</div>
        <div class="sub">تعداد خدمات ثبت‌شده</div>
      </div>

      <div class="card">
        <div class="score-title">امتیاز عملکرد</div>
        <div class="score-box">
          <div class="score-main" id="performanceScore">۸۰</div>
          <div class="score-total">از ۱۰۰</div>
        </div>
        <div class="score-caption">نمایش خلاصه و ساده عملکرد</div>
      </div>

      <div class="card" style="grid-column: 1 / -1;">
        <div class="record-title">رکورد روزانه</div>
        <div class="record-amount" id="recordAmount">۲,۰۰۰,۰۰۰ تومان</div>
        <div class="record-date" id="recordDate">ثبت‌شده در ۱۴۰۵/۰۱/۲۱</div>
      </div>
    </div>

    <div class="card search-card">
      <label class="search-label" for="serviceSearch">جستجوی خدمت</label>
      <div class="search-wrap">
        <input
          id="serviceSearch"
          class="search-input"
          type="text"
          placeholder="مثلاً جوشکاری"
        />
      </div>
      <div class="service-list" id="serviceList"></div>
    </div>

    <div class="card form-card" id="formCard">
      <div class="selected-service" id="selectedServiceBox">خدمت انتخاب نشده است</div>

      <div class="form-grid">
        <div class="field">
          <label for="countInput">تعداد</label>
          <input id="countInput" type="number" min="1" value="1" />
        </div>

        <div class="field">
          <label for="amountInput">مبلغ (تومان)</label>
          <input id="amountInput" type="number" min="0" placeholder="مثلاً 250000" />
        </div>
      </div>

      <button class="details-toggle" id="detailsToggle" type="button">افزودن توضیحات</button>

      <div class="details-box" id="detailsBox">
        <div class="field">
          <label for="descInput">توضیحات</label>
          <textarea id="descInput" rows="3" placeholder="اگر توضیحی لازم است اینجا بنویس"></textarea>
        </div>
      </div>

      <button class="submit-btn" id="submitBtn" type="button">ثبت خدمت</button>
    </div>

    <div class="today-list-title">ثبت‌های امروز</div>
    <div class="today-list" id="todayList"></div>
  </div>

  <script>
    const services = [
      { id: 1, name: "رنگ میز لبه‌دار ۳۵ تا ۱۰۰ سانت", price: 350000 },
      { id: 2, name: "جوشکاری", price: 450000 },
      { id: 3, name: "نجاری", price: 300000 }
    ];

    let selectedService = null;
    let todayRecords = JSON.parse(localStorage.getItem("todayRecords")) || [];

    const serviceSearch = document.getElementById("serviceSearch");
    const serviceList = document.getElementById("serviceList");
    const formCard = document.getElementById("formCard");
    const selectedServiceBox = document.getElementById("selectedServiceBox");
    const countInput = document.getElementById("countInput");
    const amountInput = document.getElementById("amountInput");
    const descInput = document.getElementById("descInput");
    const submitBtn = document.getElementById("submitBtn");
    const todayAmount = document.getElementById("todayAmount");
    const todayCount = document.getElementById("todayCount");
    const todayList = document.getElementById("todayList");
    const detailsToggle = document.getElementById("detailsToggle");
    const detailsBox = document.getElementById("detailsBox");

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

    function formatPrice(num) {
      return toPersianNumber(Number(num || 0).toLocaleString("en-US")) + " تومان";
    }

    function escapeHtml(text) {
      return text
        .replaceAll("&", "&")
        .replaceAll("<", "<")
        .replaceAll(">", ">")
        .replaceAll('"', """)
        .replaceAll("'", "'");
    }

    function renderServices(list) {
      if (!list.length) {
        serviceList.innerHTML = `<div class="empty-search">خدمتی پیدا نشد</div>`;
        return;
      }

      serviceList.innerHTML = list.map(service => `
        <div class="service-item" data-id="${service.id}">
          <div class="service-name">${service.name}</div>
          <div class="service-price">مبلغ پیشنهادی: ${formatPrice(service.price)}</div>
        </div>
      `).join("");

      document.querySelectorAll(".service-item").forEach(item => {
        item.addEventListener("click", () => {
          const id = Number(item.dataset.id);
          const service = services.find(s => s.id === id);
          selectedService = service;

          selectedServiceBox.textContent = `خدمت انتخاب‌شده: ${service.name}`;
          amountInput.value = service.price;
          countInput.value = 1;
          descInput.value = "";
          formCard.classList.add("active");
          serviceSearch.value = service.name;
          serviceList.innerHTML = "";
        });
      });
    }

    function updateSummary() {
      const totalAmount = todayRecords.reduce((sum, item) => sum + item.total, 0);
      const totalCount = todayRecords.reduce((sum, item) => sum + item.count, 0);

      todayAmount.textContent = formatPrice(totalAmount);
      todayCount.textContent = toPersianNumber(totalCount);
    }

    function renderTodayList() {
      if (!todayRecords.length) {
        todayList.innerHTML = `<div class="muted">هنوز هیچ خدمتی برای امروز ثبت نشده است</div>`;
        return;
      }

      todayList.innerHTML = [...todayRecords].reverse().map(item => `
        <div class="today-item">
          <div class="today-top">
            <div class="today-service">${item.serviceName}</div>
            <div class="today-total">${formatPrice(item.total)}</div>
          </div>
          <div class="today-meta">
            <div>تعداد: ${toPersianNumber(item.count)}</div>
            <div>مبلغ واحد: ${formatPrice(item.unitPrice)}</div>
          </div>
          ${item.description ? `<div class="today-desc">${escapeHtml(item.description)}</div>` : ""}
        </div>
      `).join("");
    }

    function saveRecords() {
      localStorage.setItem("todayRecords", JSON.stringify(todayRecords));
    }

    serviceSearch.addEventListener("input", () => {
      const q = serviceSearch.value.trim();
      if (!q) {
        renderServices(services);
        return;
      }

      const filtered = services.filter(service => service.name.includes(q));
      renderServices(filtered);
    });

    detailsToggle.addEventListener("click", () => {
      detailsBox.classList.toggle("open");
      detailsToggle.textContent = detailsBox.classList.contains("open")
        ? "بستن توضیحات"
        : "افزودن توضیحات";
    });

    submitBtn.addEventListener("click", () => {
      if (!selectedService) {
        alert("لطفاً ابتدا یک خدمت انتخاب کنید.");
        return;
      }

      const count = Number(countInput.value);
      const unitPrice = Number(amountInput.value);
      const description = descInput.value.trim();

      if (!count || count < 1) {
        alert("تعداد معتبر وارد کنید.");
        return;
      }

      if (isNaN(unitPrice) || unitPrice < 0) {
        alert("مبلغ معتبر وارد کنید.");
        return;
      }

      const record = {
        serviceId: selectedService.id,
        serviceName: selectedService.name,
        count,
        unitPrice,
        total: count * unitPrice,
        description
      };

      todayRecords.push(record);
      saveRecords();
      updateSummary();
      renderTodayList();

      selectedService = null;
      formCard.classList.remove("active");
      selectedServiceBox.textContent = "خدمت انتخاب نشده است";
      serviceSearch.value = "";
      countInput.value = 1;
      amountInput.value = "";
      descInput.value = "";
      detailsBox.classList.remove("open");
      detailsToggle.textContent = "افزودن توضیحات";
      renderServices(services);
    });

    renderServices(services);
    updateSummary();
    renderTodayList();
  </script>
</body>
</html>
نمونه اصلی
TEXT - 2026-05-11 23:37:16
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
خخخخ
TEXT - 2026-05-11 23:37:07
<!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ثبت کار امروز</title> <style> * { box-sizing: border-box; } body { margin: 0; padding: 14px; background: #f3f6fb; font-family: Tahoma, Arial, sans-serif; color: #111827; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card, .form-card, .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-card { background: #ffffff; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: block; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .feedback-card { background: linear-gradient(135deg, #eff6ff, #f8fafc); border: 1px solid #bfdbfe; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; box-shadow: 0 8px 20px rgba(37, 99, 235, .08); } .feedback-title { font-size: 12px; font-weight: 900; color: #1d4ed8; margin-bottom: 7px; } .feedback-main { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 5px; } .feedback-sub { font-size: 11px; line-height: 1.8; color: #4b5563; } .feedback-badge { display: inline-block; margin-top: 8px; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; } .feedback-badge.positive { background: #dcfce7; color: #166534; } .feedback-badge.negative { background: #fef3c7; color: #92400e; } .feedback-badge.neutral { background: #e5e7eb; color: #374151; } .form-card { display: none; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width: 380px) { body { padding: 10px; } .summary-card strong { font-size: 14px; } } </style> </head> <body> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً شستشو، نصب، تعمیر..." /> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">هنوز رکوردی ثبت نشده</strong> <small id="recordMessage">امروز می‌تونی اولین رکوردت رو ثبت کنی.</small> </div> </div> <div class="feedback-card"> <div class="feedback-title">آخرین بازخورد عملکرد</div> <div class="feedback-main" id="feedbackMain">هنوز بازخوردی برای نمایش ثبت نشده</div> <div class="feedback-sub" id="feedbackSub">وقتی بازخورد مثبت یا نیاز به اصلاح ثبت شود، اینجا به شکل کوتاه و محترمانه نمایش داده می‌شود.</div> <div class="feedback-badge neutral" id="feedbackBadge">بدون بازخورد</div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1" /> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0" /> </div> </div> <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div> </div> </div> </div> <script> const services = [ { name: "شستشوی کامل", price: 150000 }, { name: "نصب قطعه", price: 200000 }, { name: "تعمیر سبک", price: 180000 }, { name: "تعمیر کامل", price: 350000 }, { name: "بازدید و عیب‌یابی", price: 100000 }, { name: "تعویض قطعه", price: 250000 } ]; let selectedService = null; let records = []; let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || { count: 0, date: null }; let latestFeedback = JSON.parse(localStorage.getItem("workerLatestFeedback")) || { type: "neutral", title: "هنوز بازخوردی برای نمایش ثبت نشده", description: "وقتی بازخورد مثبت یا نیاز به اصلاح ثبت شود، اینجا به شکل کوتاه و محترمانه نمایش داده می‌شود.", badge: "بدون بازخورد" }; const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); const feedbackMain = document.getElementById("feedbackMain"); const feedbackSub = document.getElementById("feedbackSub"); const feedbackBadge = document.getElementById("feedbackBadge"); function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function getTodayDateKey() { const now = new Date(); return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate(); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); updatePersonalRecord(totalCount); } function updatePersonalRecord(totalCount) { const todayKey = getTodayDateKey(); if (totalCount > bestRecord.count) { bestRecord = { count: totalCount, date: todayKey }; localStorage.setItem("workerBestRecord", JSON.stringify(bestRecord)); bestRecordText.textContent = "رکورد جدید: " + toPersianNumber(bestRecord.count) + " کار در امروز"; recordMessage.textContent = "عالیه! امروز رکورد خودت رو شکستی 👏"; return; } if (bestRecord.count === 0) { bestRecordText.textContent = "هنوز رکوردی ثبت نشده"; recordMessage.textContent = "امروز می‌تونی اولین رکوردت رو ثبت کنی."; return; } bestRecordText.textContent = "بهترین رکورد: " + toPersianNumber(bestRecord.count) + " کار در یک روز"; if (totalCount === 0) { recordMessage.textContent = "اولین کار امروزت رو ثبت کن و به رکوردت نزدیک شو."; } else if (totalCount === bestRecord.count) { recordMessage.textContent = "به رکوردت رسیدی! یکی دیگه ثبت کنی رکورد جدید می‌زنی 🔥"; } else { const diff = bestRecord.count - totalCount; if (diff > 0) { recordMessage.textContent = "فقط " + toPersianNumber(diff) + " کار تا رسیدن به رکوردت فاصله داری."; } else { recordMessage.textContent = "امروز عملکرد خیلی خوبی داشتی 👏"; } } } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${item.description ? `<div class="record-desc">${item.description}</div>` : ""} `; recordsList.appendChild(div); }); } function renderFeedback() { feedbackMain.textContent = latestFeedback.title; feedbackSub.textContent = latestFeedback.description; feedbackBadge.textContent = latestFeedback.badge; feedbackBadge.className = "feedback-badge " + latestFeedback.type; } function saveFeedback(feedback) { latestFeedback = feedback; localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback)); renderFeedback(); } /* نمونه بازخورد: برای تست اولیه یکی از این‌ها را فعال کن. بعداً می‌تونی این مقدار را از سرور بگیری. */ // نمونه مثبت: // saveFeedback({ // type: "positive", // title: "کار اخیر با کیفیت خوب ثبت شد", // description: "برای «نصب قطعه» بازخورد مثبت ثبت شده و ۳ امتیاز اضافه شده است.", // badge: "۳+ امتیاز" // }); // نمونه نیاز به اصلاح: // saveFeedback({ // type: "negative", // title: "این کار نیاز به اصلاح داشت", // description: "برای «شستشوی کامل» ۲ امتیاز کسر شده چون کیفیت کار نیاز به اصلاح داشته است.", // badge: "۲- امتیاز" // }); function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); } serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); updateSummary(); renderRecords(); renderFeedback(); </script> </body> </html>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ثبت کار امروز</title>

<style>
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 14px;
  background: #f3f6fb;
  font-family: Tahoma, Arial, sans-serif;
  color: #111827;
}

.worker-page {
  max-width: 520px;
  margin: 0 auto;
}

.page-header {
  margin-bottom: 14px;
}

.page-title {
  font-size: 18px;
  font-weight: 900;
  margin: 0 0 5px;
  color: #111827;
}

.page-subtitle {
  font-size: 12px;
  color: #6b7280;
  margin: 0;
  line-height: 1.8;
}

.search-card,
.form-card,
.records-card {
  background: #ffffff;
  border-radius: 20px;
  padding: 13px;
  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
  margin-bottom: 13px;
  border: 1px solid #e5e7eb;
}

.search-label {
  display: block;
  font-size: 12px;
  font-weight: 900;
  margin-bottom: 8px;
  color: #374151;
}

.search-input-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #f9fafb;
  border: 2px solid #2563eb;
  border-radius: 15px;
  padding: 10px 12px;
}

.search-icon {
  font-size: 17px;
}

#serviceSearch {
  width: 100%;
  border: none;
  outline: none;
  background: transparent;
  font-size: 14px;
  font-weight: 700;
  color: #111827;
}

#serviceSearch::placeholder {
  color: #9ca3af;
  font-weight: 500;
}

.service-results {
  margin-top: 10px;
  display: none;
}

.service-result-item {
  background: #f8fafc;
  border: 1px solid #e5e7eb;
  border-radius: 13px;
  padding: 10px;
  margin-bottom: 7px;
  cursor: pointer;
}

.service-result-item:hover {
  background: #eef2ff;
  border-color: #c7d2fe;
}

.service-result-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 3px;
}

.service-result-price {
  font-size: 11px;
  color: #6b7280;
}

.summary-wrap {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 12px;
}

.summary-card {
  background: #ffffff;
  border-radius: 17px;
  padding: 12px;
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
  border: 1px solid #e5e7eb;
}

.summary-card span {
  display: block;
  color: #6b7280;
  font-size: 11px;
  font-weight: 700;
  margin-bottom: 6px;
}

.summary-card strong {
  display: block;
  color: #111827;
  font-size: 15px;
  font-weight: 900;
}

.personal-record-card {
  background: linear-gradient(135deg, #fff7ed, #fffbeb);
  border: 1px solid #fed7aa;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  display: flex;
  align-items: center;
  gap: 11px;
  box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
}

.record-icon {
  width: 42px;
  height: 42px;
  border-radius: 14px;
  background: #ffedd5;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 21px;
  flex-shrink: 0;
}

.record-content {
  flex: 1;
}

.record-content span {
  display: block;
  color: #9a3412;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content strong {
  display: block;
  color: #111827;
  font-size: 13px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content small {
  display: block;
  color: #92400e;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.7;
}

.feedback-card {
  background: linear-gradient(135deg, #eff6ff, #f8fafc);
  border: 1px solid #bfdbfe;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  box-shadow: 0 8px 20px rgba(37, 99, 235, .08);
}

.feedback-title {
  font-size: 12px;
  font-weight: 900;
  color: #1d4ed8;
  margin-bottom: 7px;
}

.feedback-main {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 5px;
}

.feedback-sub {
  font-size: 11px;
  line-height: 1.8;
  color: #4b5563;
}

.feedback-badge {
  display: inline-block;
  margin-top: 8px;
  padding: 5px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 900;
}

.feedback-badge.positive {
  background: #dcfce7;
  color: #166534;
}

.feedback-badge.negative {
  background: #fef3c7;
  color: #92400e;
}

.feedback-badge.neutral {
  background: #e5e7eb;
  color: #374151;
}

.form-card {
  display: none;
}

.selected-service {
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 12px;
}

.selected-service span {
  display: block;
  color: #1d4ed8;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.selected-service strong {
  display: block;
  color: #111827;
  font-size: 14px;
  font-weight: 900;
}

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.field {
  margin-bottom: 10px;
}

.field label {
  display: block;
  font-size: 11px;
  font-weight: 900;
  color: #374151;
  margin-bottom: 6px;
}

.field input,
.field textarea {
  width: 100%;
  border: 1px solid #d1d5db;
  outline: none;
  background: #f9fafb;
  border-radius: 13px;
  padding: 10px;
  font-size: 13px;
  font-family: inherit;
}

.field input:focus,
.field textarea:focus {
  border-color: #2563eb;
  background: #ffffff;
}

.field textarea {
  min-height: 75px;
  resize: vertical;
  line-height: 1.8;
}

.details-toggle {
  width: 100%;
  border: none;
  background: #f3f4f6;
  color: #374151;
  border-radius: 13px;
  padding: 10px;
  font-size: 12px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
  margin-bottom: 10px;
}

.details-box {
  display: none;
}

.submit-btn {
  width: 100%;
  border: none;
  background: #2563eb;
  color: #ffffff;
  border-radius: 15px;
  padding: 12px;
  font-size: 14px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
}

.records-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.records-title strong {
  font-size: 14px;
  font-weight: 900;
  color: #111827;
}

.records-title span {
  font-size: 11px;
  color: #6b7280;
  font-weight: 700;
}

.empty-records {
  background: #f9fafb;
  color: #6b7280;
  text-align: center;
  border-radius: 14px;
  padding: 16px 10px;
  font-size: 12px;
  line-height: 1.8;
}

.record-item {
  border: 1px solid #e5e7eb;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 9px;
  background: #ffffff;
}

.record-item:last-child {
  margin-bottom: 0;
}

.record-top {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 7px;
}

.record-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
}

.record-time {
  font-size: 10px;
  color: #9ca3af;
  white-space: nowrap;
}

.record-info {
  font-size: 11px;
  color: #4b5563;
  line-height: 1.9;
}

.record-total {
  margin-top: 6px;
  font-size: 12px;
  font-weight: 900;
  color: #16a34a;
}

.record-desc {
  margin-top: 5px;
  color: #6b7280;
  font-size: 11px;
  line-height: 1.8;
}

@media (max-width: 380px) {
  body {
    padding: 10px;
  }

  .summary-card strong {
    font-size: 14px;
  }
}
</style>
</head>
<body>

<div class="worker-page">

  <div class="page-header">
    <h1 class="page-title">ثبت کار امروز</h1>
    <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
  </div>

  <div class="search-card">
    <label class="search-label">جستجوی خدمت</label>

    <div class="search-input-wrap">
      <div class="search-icon">🔍</div>
      <input type="text" id="serviceSearch" placeholder="مثلاً شستشو، نصب، تعمیر..." />
    </div>

    <div class="service-results" id="serviceResults"></div>
  </div>

  <div class="summary-wrap">
    <div class="summary-card">
      <span>مبلغ امروز</span>
      <strong id="todayAmount">۰ تومان</strong>
    </div>

    <div class="summary-card">
      <span>تعداد امروز</span>
      <strong id="todayCount">۰</strong>
    </div>
  </div>

  <div class="personal-record-card">
    <div class="record-icon">🏆</div>
    <div class="record-content">
      <span>رکورد روزانه تو</span>
      <strong id="bestRecordText">هنوز رکوردی ثبت نشده</strong>
      <small id="recordMessage">امروز می‌تونی اولین رکوردت رو ثبت کنی.</small>
    </div>
  </div>

  <div class="feedback-card">
    <div class="feedback-title">آخرین بازخورد عملکرد</div>
    <div class="feedback-main" id="feedbackMain">هنوز بازخوردی برای نمایش ثبت نشده</div>
    <div class="feedback-sub" id="feedbackSub">وقتی بازخورد مثبت یا نیاز به اصلاح ثبت شود، اینجا به شکل کوتاه و محترمانه نمایش داده می‌شود.</div>
    <div class="feedback-badge neutral" id="feedbackBadge">بدون بازخورد</div>
  </div>

  <div class="form-card" id="serviceForm">
    <div class="selected-service">
      <span>خدمت انتخاب شده</span>
      <strong id="selectedServiceName">---</strong>
    </div>

    <div class="form-grid">
      <div class="field">
        <label>تعداد</label>
        <input type="number" id="serviceCount" min="1" value="1" />
      </div>

      <div class="field">
        <label>مقدار / مبلغ واحد</label>
        <input type="number" id="servicePrice" min="0" />
      </div>
    </div>

    <button type="button" class="details-toggle" id="detailsToggle">افزودن توضیحات اختیاری</button>

    <div class="details-box" id="detailsBox">
      <div class="field">
        <label>توضیحات</label>
        <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
      </div>
    </div>

    <button type="button" class="submit-btn" id="submitService">ثبت خدمت</button>
  </div>

  <div class="records-card">
    <div class="records-title">
      <strong>ثبت‌های امروز</strong>
      <span id="recordsCountText">۰ مورد</span>
    </div>

    <div id="recordsList">
      <div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>
    </div>
  </div>

</div>

<script>
const services = [
  { name: "شستشوی کامل", price: 150000 },
  { name: "نصب قطعه", price: 200000 },
  { name: "تعمیر سبک", price: 180000 },
  { name: "تعمیر کامل", price: 350000 },
  { name: "بازدید و عیب‌یابی", price: 100000 },
  { name: "تعویض قطعه", price: 250000 }
];

let selectedService = null;
let records = [];

let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || {
  count: 0,
  date: null
};

let latestFeedback = JSON.parse(localStorage.getItem("workerLatestFeedback")) || {
  type: "neutral",
  title: "هنوز بازخوردی برای نمایش ثبت نشده",
  description: "وقتی بازخورد مثبت یا نیاز به اصلاح ثبت شود، اینجا به شکل کوتاه و محترمانه نمایش داده می‌شود.",
  badge: "بدون بازخورد"
};

const serviceSearch = document.getElementById("serviceSearch");
const serviceResults = document.getElementById("serviceResults");
const serviceForm = document.getElementById("serviceForm");
const selectedServiceName = document.getElementById("selectedServiceName");
const serviceCount = document.getElementById("serviceCount");
const servicePrice = document.getElementById("servicePrice");
const serviceDescription = document.getElementById("serviceDescription");
const submitService = document.getElementById("submitService");
const todayAmount = document.getElementById("todayAmount");
const todayCount = document.getElementById("todayCount");
const recordsList = document.getElementById("recordsList");
const recordsCountText = document.getElementById("recordsCountText");
const detailsToggle = document.getElementById("detailsToggle");
const detailsBox = document.getElementById("detailsBox");
const bestRecordText = document.getElementById("bestRecordText");
const recordMessage = document.getElementById("recordMessage");
const feedbackMain = document.getElementById("feedbackMain");
const feedbackSub = document.getElementById("feedbackSub");
const feedbackBadge = document.getElementById("feedbackBadge");

function toPersianNumber(value) {
  return Number(value || 0).toLocaleString("fa-IR");
}

function formatToman(value) {
  return toPersianNumber(value) + " تومان";
}

function getTodayDateKey() {
  const now = new Date();
  return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate();
}

function showResults(keyword) {
  const text = keyword.trim();
  serviceResults.innerHTML = "";

  if (!text) {
    serviceResults.style.display = "none";
    return;
  }

  const filtered = services.filter(function(service) {
    return service.name.includes(text);
  });

  if (filtered.length === 0) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
      <div class="service-result-price">برای انتخاب این مورد بزنید</div>
    `;
    item.addEventListener("click", function() {
      selectService({ name: text, price: 0 });
    });
    serviceResults.appendChild(item);
    serviceResults.style.display = "block";
    return;
  }

  filtered.forEach(function(service) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">${service.name}</div>
      <div class="service-result-price">${formatToman(service.price)}</div>
    `;
    item.addEventListener("click", function() {
      selectService(service);
    });
    serviceResults.appendChild(item);
  });

  serviceResults.style.display = "block";
}

function selectService(service) {
  selectedService = service;
  selectedServiceName.textContent = service.name;
  serviceSearch.value = service.name;
  servicePrice.value = service.price || "";
  serviceCount.value = 1;
  serviceDescription.value = "";
  serviceResults.style.display = "none";
  serviceForm.style.display = "block";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";

  setTimeout(function() {
    serviceCount.focus();
  }, 100);
}

function updateSummary() {
  const totalAmount = records.reduce(function(sum, item) {
    return sum + item.total;
  }, 0);

  const totalCount = records.reduce(function(sum, item) {
    return sum + item.count;
  }, 0);

  todayAmount.textContent = formatToman(totalAmount);
  todayCount.textContent = toPersianNumber(totalCount);

  updatePersonalRecord(totalCount);
}

function updatePersonalRecord(totalCount) {
  const todayKey = getTodayDateKey();

  if (totalCount > bestRecord.count) {
    bestRecord = {
      count: totalCount,
      date: todayKey
    };

    localStorage.setItem("workerBestRecord", JSON.stringify(bestRecord));
    bestRecordText.textContent = "رکورد جدید: " + toPersianNumber(bestRecord.count) + " کار در امروز";
    recordMessage.textContent = "عالیه! امروز رکورد خودت رو شکستی 👏";
    return;
  }

  if (bestRecord.count === 0) {
    bestRecordText.textContent = "هنوز رکوردی ثبت نشده";
    recordMessage.textContent = "امروز می‌تونی اولین رکوردت رو ثبت کنی.";
    return;
  }

  bestRecordText.textContent = "بهترین رکورد: " + toPersianNumber(bestRecord.count) + " کار در یک روز";

  if (totalCount === 0) {
    recordMessage.textContent = "اولین کار امروزت رو ثبت کن و به رکوردت نزدیک شو.";
  } else if (totalCount === bestRecord.count) {
    recordMessage.textContent = "به رکوردت رسیدی! یکی دیگه ثبت کنی رکورد جدید می‌زنی 🔥";
  } else {
    const diff = bestRecord.count - totalCount;
    if (diff > 0) {
      recordMessage.textContent = "فقط " + toPersianNumber(diff) + " کار تا رسیدن به رکوردت فاصله داری.";
    } else {
      recordMessage.textContent = "امروز عملکرد خیلی خوبی داشتی 👏";
    }
  }
}

function renderRecords() {
  recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

  if (records.length === 0) {
    recordsList.innerHTML = `<div class="empty-records">هنوز کاری برای امروز ثبت نشده.</div>`;
    return;
  }

  recordsList.innerHTML = "";

  const reversed = records.slice().reverse();
  reversed.forEach(function(item) {
    const div = document.createElement("div");
    div.className = "record-item";
    div.innerHTML = `
      <div class="record-top">
        <div class="record-name">${item.name}</div>
        <div class="record-time">${item.time}</div>
      </div>
      <div class="record-info">
        تعداد: ${toPersianNumber(item.count)} |
        مبلغ واحد: ${formatToman(item.price)}
      </div>
      <div class="record-total">
        جمع: ${formatToman(item.total)}
      </div>
      ${item.description ? `<div class="record-desc">${item.description}</div>` : ""}
    `;
    recordsList.appendChild(div);
  });
}

function renderFeedback() {
  feedbackMain.textContent = latestFeedback.title;
  feedbackSub.textContent = latestFeedback.description;
  feedbackBadge.textContent = latestFeedback.badge;
  feedbackBadge.className = "feedback-badge " + latestFeedback.type;
}

function saveFeedback(feedback) {
  latestFeedback = feedback;
  localStorage.setItem("workerLatestFeedback", JSON.stringify(latestFeedback));
  renderFeedback();
}

/*
  نمونه بازخورد:
  برای تست اولیه یکی از این‌ها را فعال کن.
  بعداً می‌تونی این مقدار را از سرور بگیری.
*/

// نمونه مثبت:
// saveFeedback({
//   type: "positive",
//   title: "کار اخیر با کیفیت خوب ثبت شد",
//   description: "برای «نصب قطعه» بازخورد مثبت ثبت شده و ۳ امتیاز اضافه شده است.",
//   badge: "۳+ امتیاز"
// });

// نمونه نیاز به اصلاح:
// saveFeedback({
//   type: "negative",
//   title: "این کار نیاز به اصلاح داشت",
//   description: "برای «شستشوی کامل» ۲ امتیاز کسر شده چون کیفیت کار نیاز به اصلاح داشته است.",
//   badge: "۲- امتیاز"
// });

function submitRecord() {
  if (!selectedService) {
    alert("اول یک خدمت را انتخاب کن.");
    return;
  }

  const count = parseInt(serviceCount.value, 10);
  const price = parseInt(servicePrice.value, 10);
  const description = serviceDescription.value.trim();

  if (!count || count <= 0) {
    alert("تعداد را درست وارد کن.");
    return;
  }

  if (isNaN(price) || price < 0) {
    alert("مبلغ را درست وارد کن.");
    return;
  }

  const total = count * price;
  const now = new Date();

  records.push({
    name: selectedService.name,
    count: count,
    price: price,
    total: total,
    description: description,
    time: now.toLocaleTimeString("fa-IR", {
      hour: "2-digit",
      minute: "2-digit"
    })
  });

  renderRecords();
  updateSummary();

  selectedService = null;
  serviceSearch.value = "";
  serviceCount.value = 1;
  servicePrice.value = "";
  serviceDescription.value = "";
  selectedServiceName.textContent = "---";
  serviceForm.style.display = "none";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";
  serviceSearch.focus();
}

serviceSearch.addEventListener("input", function() {
  showResults(serviceSearch.value);
});

detailsToggle.addEventListener("click", function() {
  if (detailsBox.style.display === "block") {
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
  } else {
    detailsBox.style.display = "block";
    detailsToggle.textContent = "بستن توضیحات";
  }
});

submitService.addEventListener("click", submitRecord);

updateSummary();
renderRecords();
renderFeedback();
</script>

</body>
</html>
نمونه اصلی
TEXT - 2026-05-11 23:28:03
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
نسل جدید
TEXT - 2026-05-11 23:27:57
<!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ثبت کار امروز</title> <style> * { box-sizing: border-box; } body { margin: 0; padding: 14px; background: #f3f6fb; font-family: Tahoma, Arial, sans-serif; color: #111827; } .worker-page { max-width: 520px; margin: 0 auto; } .page-header { margin-bottom: 14px; } .page-title { font-size: 18px; font-weight: 900; margin: 0 0 5px; color: #111827; } .page-subtitle { font-size: 12px; color: #6b7280; margin: 0; line-height: 1.8; } .search-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); margin-bottom: 13px; border: 1px solid #e5e7eb; } .search-label { display: block; font-size: 12px; font-weight: 900; margin-bottom: 8px; color: #374151; } .search-input-wrap { display: flex; align-items: center; gap: 8px; background: #f9fafb; border: 2px solid #2563eb; border-radius: 15px; padding: 10px 12px; } .search-icon { font-size: 17px; } #serviceSearch { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; font-weight: 700; color: #111827; } #serviceSearch::placeholder { color: #9ca3af; font-weight: 500; } .service-results { margin-top: 10px; display: none; } .service-result-item { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 13px; padding: 10px; margin-bottom: 7px; cursor: pointer; } .service-result-item:hover { background: #eef2ff; border-color: #c7d2fe; } .service-result-name { font-size: 13px; font-weight: 900; color: #111827; margin-bottom: 3px; } .service-result-price { font-size: 11px; color: #6b7280; } .summary-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; } .summary-card { background: #ffffff; border-radius: 17px; padding: 12px; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07); border: 1px solid #e5e7eb; } .summary-card span { display: block; color: #6b7280; font-size: 11px; font-weight: 700; margin-bottom: 6px; } .summary-card strong { display: block; color: #111827; font-size: 15px; font-weight: 900; } .personal-record-card { background: linear-gradient(135deg, #fff7ed, #fffbeb); border: 1px solid #fed7aa; border-radius: 18px; padding: 12px 13px; margin-bottom: 13px; display: flex; align-items: center; gap: 11px; box-shadow: 0 8px 20px rgba(251, 146, 60, .12); } .record-icon { width: 42px; height: 42px; border-radius: 14px; background: #ffedd5; display: flex; align-items: center; justify-content: center; font-size: 21px; flex-shrink: 0; } .record-content { flex: 1; } .record-content span { display: block; color: #9a3412; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .record-content strong { display: block; color: #111827; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .record-content small { display: block; color: #92400e; font-size: 11px; font-weight: 700; line-height: 1.7; } .form-card { display: none; background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); border: 1px solid #e5e7eb; margin-bottom: 13px; } .selected-service { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 15px; padding: 11px; margin-bottom: 12px; } .selected-service span { display: block; color: #1d4ed8; font-size: 11px; font-weight: 900; margin-bottom: 4px; } .selected-service strong { display: block; color: #111827; font-size: 14px; font-weight: 900; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .field { margin-bottom: 10px; } .field label { display: block; font-size: 11px; font-weight: 900; color: #374151; margin-bottom: 6px; } .field input, .field textarea { width: 100%; border: 1px solid #d1d5db; outline: none; background: #f9fafb; border-radius: 13px; padding: 10px; font-size: 13px; font-family: inherit; } .field input:focus, .field textarea:focus { border-color: #2563eb; background: #ffffff; } .field textarea { min-height: 75px; resize: vertical; line-height: 1.8; } .details-toggle { width: 100%; border: none; background: #f3f4f6; color: #374151; border-radius: 13px; padding: 10px; font-size: 12px; font-weight: 900; font-family: inherit; cursor: pointer; margin-bottom: 10px; } .details-box { display: none; } .submit-btn { width: 100%; border: none; background: #2563eb; color: #ffffff; border-radius: 15px; padding: 12px; font-size: 14px; font-weight: 900; font-family: inherit; cursor: pointer; } .submit-btn:active { transform: scale(.99); } .records-card { background: #ffffff; border-radius: 20px; padding: 13px; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); border: 1px solid #e5e7eb; } .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .records-title strong { font-size: 14px; font-weight: 900; color: #111827; } .records-title span { font-size: 11px; color: #6b7280; font-weight: 700; } .empty-records { background: #f9fafb; color: #6b7280; text-align: center; border-radius: 14px; padding: 16px 10px; font-size: 12px; line-height: 1.8; } .record-item { border: 1px solid #e5e7eb; border-radius: 15px; padding: 11px; margin-bottom: 9px; background: #ffffff; } .record-item:last-child { margin-bottom: 0; } .record-top { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .record-name { font-size: 13px; font-weight: 900; color: #111827; } .record-time { font-size: 10px; color: #9ca3af; white-space: nowrap; } .record-info { font-size: 11px; color: #4b5563; line-height: 1.9; } .record-total { margin-top: 6px; font-size: 12px; font-weight: 900; color: #16a34a; } .record-desc { margin-top: 5px; color: #6b7280; font-size: 11px; line-height: 1.8; } @media (max-width: 380px) { body { padding: 10px; } .summary-card strong { font-size: 14px; } } </style> </head> <body> <div class="worker-page"> <div class="page-header"> <h1 class="page-title">ثبت کار امروز</h1> <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p> </div> <div class="search-card"> <label class="search-label">جستجوی خدمت</label> <div class="search-input-wrap"> <div class="search-icon">🔍</div> <input type="text" id="serviceSearch" placeholder="مثلاً شستشو، نصب، تعمیر..."> </div> <div class="service-results" id="serviceResults"></div> </div> <div class="summary-wrap"> <div class="summary-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="summary-card"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> </div> <div class="personal-record-card"> <div class="record-icon">🏆</div> <div class="record-content"> <span>رکورد روزانه تو</span> <strong id="bestRecordText">هنوز رکوردی ثبت نشده</strong> <small id="recordMessage">امروز می‌تونی اولین رکوردت رو ثبت کنی.</small> </div> </div> <div class="form-card" id="serviceForm"> <div class="selected-service"> <span>خدمت انتخاب شده</span> <strong id="selectedServiceName">---</strong> </div> <div class="form-grid"> <div class="field"> <label>تعداد</label> <input type="number" id="serviceCount" min="1" value="1"> </div> <div class="field"> <label>مقدار / مبلغ واحد</label> <input type="number" id="servicePrice" min="0"> </div> </div> <button type="button" class="details-toggle" id="detailsToggle"> افزودن توضیحات اختیاری </button> <div class="details-box" id="detailsBox"> <div class="field"> <label>توضیحات</label> <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea> </div> </div> <button type="button" class="submit-btn" id="submitService"> ثبت خدمت </button> </div> <div class="records-card"> <div class="records-title"> <strong>ثبت‌های امروز</strong> <span id="recordsCountText">۰ مورد</span> </div> <div id="recordsList"> <div class="empty-records"> هنوز کاری برای امروز ثبت نشده. </div> </div> </div> </div> <script> const services = [ { name: "شستشوی کامل", price: 150000 }, { name: "نصب قطعه", price: 200000 }, { name: "تعمیر سبک", price: 180000 }, { name: "تعمیر کامل", price: 350000 }, { name: "بازدید و عیب‌یابی", price: 100000 }, { name: "تعویض قطعه", price: 250000 } ]; let selectedService = null; let records = []; let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || { count: 0, date: null }; const serviceSearch = document.getElementById("serviceSearch"); const serviceResults = document.getElementById("serviceResults"); const serviceForm = document.getElementById("serviceForm"); const selectedServiceName = document.getElementById("selectedServiceName"); const serviceCount = document.getElementById("serviceCount"); const servicePrice = document.getElementById("servicePrice"); const serviceDescription = document.getElementById("serviceDescription"); const submitService = document.getElementById("submitService"); const todayAmount = document.getElementById("todayAmount"); const todayCount = document.getElementById("todayCount"); const recordsList = document.getElementById("recordsList"); const recordsCountText = document.getElementById("recordsCountText"); const detailsToggle = document.getElementById("detailsToggle"); const detailsBox = document.getElementById("detailsBox"); const bestRecordText = document.getElementById("bestRecordText"); const recordMessage = document.getElementById("recordMessage"); function toPersianNumber(value) { return Number(value || 0).toLocaleString("fa-IR"); } function formatToman(value) { return toPersianNumber(value) + " تومان"; } function getTodayDateKey() { const now = new Date(); return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate(); } function showResults(keyword) { const text = keyword.trim(); serviceResults.innerHTML = ""; if (!text) { serviceResults.style.display = "none"; return; } const filtered = services.filter(function(service) { return service.name.includes(text); }); if (filtered.length === 0) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">ثبت خدمت جدید: ${text}</div> <div class="service-result-price">برای انتخاب این مورد بزنید</div> `; item.addEventListener("click", function() { selectService({ name: text, price: 0 }); }); serviceResults.appendChild(item); serviceResults.style.display = "block"; return; } filtered.forEach(function(service) { const item = document.createElement("div"); item.className = "service-result-item"; item.innerHTML = ` <div class="service-result-name">${service.name}</div> <div class="service-result-price">${formatToman(service.price)}</div> `; item.addEventListener("click", function() { selectService(service); }); serviceResults.appendChild(item); }); serviceResults.style.display = "block"; } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; serviceSearch.value = service.name; servicePrice.value = service.price || ""; serviceCount.value = 1; serviceDescription.value = ""; serviceResults.style.display = "none"; serviceForm.style.display = "block"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; setTimeout(function() { serviceCount.focus(); }, 100); } function updateSummary() { const totalAmount = records.reduce(function(sum, item) { return sum + item.total; }, 0); const totalCount = records.reduce(function(sum, item) { return sum + item.count; }, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = toPersianNumber(totalCount); updatePersonalRecord(totalCount); } function updatePersonalRecord(totalCount) { const todayKey = getTodayDateKey(); if (totalCount > bestRecord.count) { bestRecord = { count: totalCount, date: todayKey }; localStorage.setItem("workerBestRecord", JSON.stringify(bestRecord)); bestRecordText.textContent = "رکورد جدید: " + toPersianNumber(bestRecord.count) + " کار در امروز"; recordMessage.textContent = "عالیه! امروز رکورد خودت رو شکستی 👏"; return; } if (bestRecord.count === 0) { bestRecordText.textContent = "هنوز رکوردی ثبت نشده"; recordMessage.textContent = "امروز می‌تونی اولین رکوردت رو ثبت کنی."; return; } bestRecordText.textContent = "بهترین رکورد: " + toPersianNumber(bestRecord.count) + " کار در یک روز"; if (totalCount === 0) { recordMessage.textContent = "اولین کار امروزت رو ثبت کن و به رکوردت نزدیک شو."; } else if (totalCount === bestRecord.count) { recordMessage.textContent = "به رکوردت رسیدی! یکی دیگه ثبت کنی رکورد جدید می‌زنی 🔥"; } else { const diff = bestRecord.count - totalCount; if (diff > 0) { recordMessage.textContent = "فقط " + toPersianNumber(diff) + " کار تا رسیدن به رکوردت فاصله داری."; } else { recordMessage.textContent = "امروز عملکرد خیلی خوبی داشتی 👏"; } } } function renderRecords() { recordsCountText.textContent = toPersianNumber(records.length) + " مورد"; if (records.length === 0) { recordsList.innerHTML = ` <div class="empty-records"> هنوز کاری برای امروز ثبت نشده. </div> `; return; } recordsList.innerHTML = ""; const reversed = records.slice().reverse(); reversed.forEach(function(item) { const div = document.createElement("div"); div.className = "record-item"; div.innerHTML = ` <div class="record-top"> <div class="record-name">${item.name}</div> <div class="record-time">${item.time}</div> </div> <div class="record-info"> تعداد: ${toPersianNumber(item.count)} | مبلغ واحد: ${formatToman(item.price)} </div> <div class="record-total"> جمع: ${formatToman(item.total)} </div> ${ item.description ? `<div class="record-desc">${item.description}</div>` : "" } `; recordsList.appendChild(div); }); } function submitRecord() { if (!selectedService) { alert("اول یک خدمت را انتخاب کن."); return; } const count = parseInt(serviceCount.value, 10); const price = parseInt(servicePrice.value, 10); const description = serviceDescription.value.trim(); if (!count || count <= 0) { alert("تعداد را درست وارد کن."); return; } if (isNaN(price) || price < 0) { alert("مبلغ را درست وارد کن."); return; } const total = count * price; const now = new Date(); records.push({ name: selectedService.name, count: count, price: price, total: total, description: description, time: now.toLocaleTimeString("fa-IR", { hour: "2-digit", minute: "2-digit" }) }); renderRecords(); updateSummary(); selectedService = null; serviceSearch.value = ""; serviceCount.value = 1; servicePrice.value = ""; serviceDescription.value = ""; selectedServiceName.textContent = "---"; serviceForm.style.display = "none"; detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; serviceSearch.focus(); } serviceSearch.addEventListener("input", function() { showResults(serviceSearch.value); }); detailsToggle.addEventListener("click", function() { if (detailsBox.style.display === "block") { detailsBox.style.display = "none"; detailsToggle.textContent = "افزودن توضیحات اختیاری"; } else { detailsBox.style.display = "block"; detailsToggle.textContent = "بستن توضیحات"; } }); submitService.addEventListener("click", submitRecord); updateSummary(); renderRecords(); </script> </body> </html>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>ثبت کار امروز</title>

<style>
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 14px;
  background: #f3f6fb;
  font-family: Tahoma, Arial, sans-serif;
  color: #111827;
}

.worker-page {
  max-width: 520px;
  margin: 0 auto;
}

.page-header {
  margin-bottom: 14px;
}

.page-title {
  font-size: 18px;
  font-weight: 900;
  margin: 0 0 5px;
  color: #111827;
}

.page-subtitle {
  font-size: 12px;
  color: #6b7280;
  margin: 0;
  line-height: 1.8;
}

.search-card {
  background: #ffffff;
  border-radius: 20px;
  padding: 13px;
  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
  margin-bottom: 13px;
  border: 1px solid #e5e7eb;
}

.search-label {
  display: block;
  font-size: 12px;
  font-weight: 900;
  margin-bottom: 8px;
  color: #374151;
}

.search-input-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #f9fafb;
  border: 2px solid #2563eb;
  border-radius: 15px;
  padding: 10px 12px;
}

.search-icon {
  font-size: 17px;
}

#serviceSearch {
  width: 100%;
  border: none;
  outline: none;
  background: transparent;
  font-size: 14px;
  font-weight: 700;
  color: #111827;
}

#serviceSearch::placeholder {
  color: #9ca3af;
  font-weight: 500;
}

.service-results {
  margin-top: 10px;
  display: none;
}

.service-result-item {
  background: #f8fafc;
  border: 1px solid #e5e7eb;
  border-radius: 13px;
  padding: 10px;
  margin-bottom: 7px;
  cursor: pointer;
}

.service-result-item:hover {
  background: #eef2ff;
  border-color: #c7d2fe;
}

.service-result-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
  margin-bottom: 3px;
}

.service-result-price {
  font-size: 11px;
  color: #6b7280;
}

.summary-wrap {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 12px;
}

.summary-card {
  background: #ffffff;
  border-radius: 17px;
  padding: 12px;
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
  border: 1px solid #e5e7eb;
}

.summary-card span {
  display: block;
  color: #6b7280;
  font-size: 11px;
  font-weight: 700;
  margin-bottom: 6px;
}

.summary-card strong {
  display: block;
  color: #111827;
  font-size: 15px;
  font-weight: 900;
}

.personal-record-card {
  background: linear-gradient(135deg, #fff7ed, #fffbeb);
  border: 1px solid #fed7aa;
  border-radius: 18px;
  padding: 12px 13px;
  margin-bottom: 13px;
  display: flex;
  align-items: center;
  gap: 11px;
  box-shadow: 0 8px 20px rgba(251, 146, 60, .12);
}

.record-icon {
  width: 42px;
  height: 42px;
  border-radius: 14px;
  background: #ffedd5;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 21px;
  flex-shrink: 0;
}

.record-content {
  flex: 1;
}

.record-content span {
  display: block;
  color: #9a3412;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content strong {
  display: block;
  color: #111827;
  font-size: 13px;
  font-weight: 900;
  margin-bottom: 4px;
}

.record-content small {
  display: block;
  color: #92400e;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.7;
}

.form-card {
  display: none;
  background: #ffffff;
  border-radius: 20px;
  padding: 13px;
  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
  border: 1px solid #e5e7eb;
  margin-bottom: 13px;
}

.selected-service {
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 12px;
}

.selected-service span {
  display: block;
  color: #1d4ed8;
  font-size: 11px;
  font-weight: 900;
  margin-bottom: 4px;
}

.selected-service strong {
  display: block;
  color: #111827;
  font-size: 14px;
  font-weight: 900;
}

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.field {
  margin-bottom: 10px;
}

.field label {
  display: block;
  font-size: 11px;
  font-weight: 900;
  color: #374151;
  margin-bottom: 6px;
}

.field input,
.field textarea {
  width: 100%;
  border: 1px solid #d1d5db;
  outline: none;
  background: #f9fafb;
  border-radius: 13px;
  padding: 10px;
  font-size: 13px;
  font-family: inherit;
}

.field input:focus,
.field textarea:focus {
  border-color: #2563eb;
  background: #ffffff;
}

.field textarea {
  min-height: 75px;
  resize: vertical;
  line-height: 1.8;
}

.details-toggle {
  width: 100%;
  border: none;
  background: #f3f4f6;
  color: #374151;
  border-radius: 13px;
  padding: 10px;
  font-size: 12px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
  margin-bottom: 10px;
}

.details-box {
  display: none;
}

.submit-btn {
  width: 100%;
  border: none;
  background: #2563eb;
  color: #ffffff;
  border-radius: 15px;
  padding: 12px;
  font-size: 14px;
  font-weight: 900;
  font-family: inherit;
  cursor: pointer;
}

.submit-btn:active {
  transform: scale(.99);
}

.records-card {
  background: #ffffff;
  border-radius: 20px;
  padding: 13px;
  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
  border: 1px solid #e5e7eb;
}

.records-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.records-title strong {
  font-size: 14px;
  font-weight: 900;
  color: #111827;
}

.records-title span {
  font-size: 11px;
  color: #6b7280;
  font-weight: 700;
}

.empty-records {
  background: #f9fafb;
  color: #6b7280;
  text-align: center;
  border-radius: 14px;
  padding: 16px 10px;
  font-size: 12px;
  line-height: 1.8;
}

.record-item {
  border: 1px solid #e5e7eb;
  border-radius: 15px;
  padding: 11px;
  margin-bottom: 9px;
  background: #ffffff;
}

.record-item:last-child {
  margin-bottom: 0;
}

.record-top {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 7px;
}

.record-name {
  font-size: 13px;
  font-weight: 900;
  color: #111827;
}

.record-time {
  font-size: 10px;
  color: #9ca3af;
  white-space: nowrap;
}

.record-info {
  font-size: 11px;
  color: #4b5563;
  line-height: 1.9;
}

.record-total {
  margin-top: 6px;
  font-size: 12px;
  font-weight: 900;
  color: #16a34a;
}

.record-desc {
  margin-top: 5px;
  color: #6b7280;
  font-size: 11px;
  line-height: 1.8;
}

@media (max-width: 380px) {
  body {
    padding: 10px;
  }

  .summary-card strong {
    font-size: 14px;
  }
}
</style>
</head>

<body>

<div class="worker-page">

  <div class="page-header">
    <h1 class="page-title">ثبت کار امروز</h1>
    <p class="page-subtitle">خدمت را جستجو کن، مقدار را وارد کن و سریع ثبت کن.</p>
  </div>

  <div class="search-card">
    <label class="search-label">جستجوی خدمت</label>

    <div class="search-input-wrap">
      <div class="search-icon">🔍</div>
      <input type="text" id="serviceSearch" placeholder="مثلاً شستشو، نصب، تعمیر...">
    </div>

    <div class="service-results" id="serviceResults"></div>
  </div>

  <div class="summary-wrap">
    <div class="summary-card">
      <span>مبلغ امروز</span>
      <strong id="todayAmount">۰ تومان</strong>
    </div>

    <div class="summary-card">
      <span>تعداد امروز</span>
      <strong id="todayCount">۰</strong>
    </div>
  </div>

  <div class="personal-record-card">
    <div class="record-icon">🏆</div>

    <div class="record-content">
      <span>رکورد روزانه تو</span>
      <strong id="bestRecordText">هنوز رکوردی ثبت نشده</strong>
      <small id="recordMessage">امروز می‌تونی اولین رکوردت رو ثبت کنی.</small>
    </div>
  </div>

  <div class="form-card" id="serviceForm">

    <div class="selected-service">
      <span>خدمت انتخاب شده</span>
      <strong id="selectedServiceName">---</strong>
    </div>

    <div class="form-grid">
      <div class="field">
        <label>تعداد</label>
        <input type="number" id="serviceCount" min="1" value="1">
      </div>

      <div class="field">
        <label>مقدار / مبلغ واحد</label>
        <input type="number" id="servicePrice" min="0">
      </div>
    </div>

    <button type="button" class="details-toggle" id="detailsToggle">
      افزودن توضیحات اختیاری
    </button>

    <div class="details-box" id="detailsBox">
      <div class="field">
        <label>توضیحات</label>
        <textarea id="serviceDescription" placeholder="اگر توضیحی لازم است اینجا بنویس..."></textarea>
      </div>
    </div>

    <button type="button" class="submit-btn" id="submitService">
      ثبت خدمت
    </button>

  </div>

  <div class="records-card">
    <div class="records-title">
      <strong>ثبت‌های امروز</strong>
      <span id="recordsCountText">۰ مورد</span>
    </div>

    <div id="recordsList">
      <div class="empty-records">
        هنوز کاری برای امروز ثبت نشده.
      </div>
    </div>
  </div>

</div>

<script>
const services = [
  {
    name: "شستشوی کامل",
    price: 150000
  },
  {
    name: "نصب قطعه",
    price: 200000
  },
  {
    name: "تعمیر سبک",
    price: 180000
  },
  {
    name: "تعمیر کامل",
    price: 350000
  },
  {
    name: "بازدید و عیب‌یابی",
    price: 100000
  },
  {
    name: "تعویض قطعه",
    price: 250000
  }
];

let selectedService = null;
let records = [];

let bestRecord = JSON.parse(localStorage.getItem("workerBestRecord")) || {
  count: 0,
  date: null
};

const serviceSearch = document.getElementById("serviceSearch");
const serviceResults = document.getElementById("serviceResults");
const serviceForm = document.getElementById("serviceForm");
const selectedServiceName = document.getElementById("selectedServiceName");
const serviceCount = document.getElementById("serviceCount");
const servicePrice = document.getElementById("servicePrice");
const serviceDescription = document.getElementById("serviceDescription");
const submitService = document.getElementById("submitService");
const todayAmount = document.getElementById("todayAmount");
const todayCount = document.getElementById("todayCount");
const recordsList = document.getElementById("recordsList");
const recordsCountText = document.getElementById("recordsCountText");
const detailsToggle = document.getElementById("detailsToggle");
const detailsBox = document.getElementById("detailsBox");
const bestRecordText = document.getElementById("bestRecordText");
const recordMessage = document.getElementById("recordMessage");

function toPersianNumber(value) {
  return Number(value || 0).toLocaleString("fa-IR");
}

function formatToman(value) {
  return toPersianNumber(value) + " تومان";
}

function getTodayDateKey() {
  const now = new Date();
  return now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate();
}

function showResults(keyword) {
  const text = keyword.trim();

  serviceResults.innerHTML = "";

  if (!text) {
    serviceResults.style.display = "none";
    return;
  }

  const filtered = services.filter(function(service) {
    return service.name.includes(text);
  });

  if (filtered.length === 0) {
    const item = document.createElement("div");
    item.className = "service-result-item";
    item.innerHTML = `
      <div class="service-result-name">ثبت خدمت جدید: ${text}</div>
      <div class="service-result-price">برای انتخاب این مورد بزنید</div>
    `;

    item.addEventListener("click", function() {
      selectService({
        name: text,
        price: 0
      });
    });

    serviceResults.appendChild(item);
    serviceResults.style.display = "block";
    return;
  }

  filtered.forEach(function(service) {
    const item = document.createElement("div");
    item.className = "service-result-item";

    item.innerHTML = `
      <div class="service-result-name">${service.name}</div>
      <div class="service-result-price">${formatToman(service.price)}</div>
    `;

    item.addEventListener("click", function() {
      selectService(service);
    });

    serviceResults.appendChild(item);
  });

  serviceResults.style.display = "block";
}

function selectService(service) {
  selectedService = service;

  selectedServiceName.textContent = service.name;
  serviceSearch.value = service.name;
  servicePrice.value = service.price || "";
  serviceCount.value = 1;
  serviceDescription.value = "";

  serviceResults.style.display = "none";
  serviceForm.style.display = "block";

  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";

  setTimeout(function() {
    serviceCount.focus();
  }, 100);
}

function updateSummary() {
  const totalAmount = records.reduce(function(sum, item) {
    return sum + item.total;
  }, 0);

  const totalCount = records.reduce(function(sum, item) {
    return sum + item.count;
  }, 0);

  todayAmount.textContent = formatToman(totalAmount);
  todayCount.textContent = toPersianNumber(totalCount);

  updatePersonalRecord(totalCount);
}

function updatePersonalRecord(totalCount) {
  const todayKey = getTodayDateKey();

  if (totalCount > bestRecord.count) {
    bestRecord = {
      count: totalCount,
      date: todayKey
    };

    localStorage.setItem("workerBestRecord", JSON.stringify(bestRecord));

    bestRecordText.textContent = "رکورد جدید: " + toPersianNumber(bestRecord.count) + " کار در امروز";
    recordMessage.textContent = "عالیه! امروز رکورد خودت رو شکستی 👏";
    return;
  }

  if (bestRecord.count === 0) {
    bestRecordText.textContent = "هنوز رکوردی ثبت نشده";
    recordMessage.textContent = "امروز می‌تونی اولین رکوردت رو ثبت کنی.";
    return;
  }

  bestRecordText.textContent = "بهترین رکورد: " + toPersianNumber(bestRecord.count) + " کار در یک روز";

  if (totalCount === 0) {
    recordMessage.textContent = "اولین کار امروزت رو ثبت کن و به رکوردت نزدیک شو.";
  } else if (totalCount === bestRecord.count) {
    recordMessage.textContent = "به رکوردت رسیدی! یکی دیگه ثبت کنی رکورد جدید می‌زنی 🔥";
  } else {
    const diff = bestRecord.count - totalCount;

    if (diff > 0) {
      recordMessage.textContent = "فقط " + toPersianNumber(diff) + " کار تا رسیدن به رکوردت فاصله داری.";
    } else {
      recordMessage.textContent = "امروز عملکرد خیلی خوبی داشتی 👏";
    }
  }
}

function renderRecords() {
  recordsCountText.textContent = toPersianNumber(records.length) + " مورد";

  if (records.length === 0) {
    recordsList.innerHTML = `
      <div class="empty-records">
        هنوز کاری برای امروز ثبت نشده.
      </div>
    `;
    return;
  }

  recordsList.innerHTML = "";

  const reversed = records.slice().reverse();

  reversed.forEach(function(item) {
    const div = document.createElement("div");
    div.className = "record-item";

    div.innerHTML = `
      <div class="record-top">
        <div class="record-name">${item.name}</div>
        <div class="record-time">${item.time}</div>
      </div>

      <div class="record-info">
        تعداد: ${toPersianNumber(item.count)}
        |
        مبلغ واحد: ${formatToman(item.price)}
      </div>

      <div class="record-total">
        جمع: ${formatToman(item.total)}
      </div>

      ${
        item.description
        ? `<div class="record-desc">${item.description}</div>`
        : ""
      }
    `;

    recordsList.appendChild(div);
  });
}

function submitRecord() {
  if (!selectedService) {
    alert("اول یک خدمت را انتخاب کن.");
    return;
  }

  const count = parseInt(serviceCount.value, 10);
  const price = parseInt(servicePrice.value, 10);
  const description = serviceDescription.value.trim();

  if (!count || count <= 0) {
    alert("تعداد را درست وارد کن.");
    return;
  }

  if (isNaN(price) || price < 0) {
    alert("مبلغ را درست وارد کن.");
    return;
  }

  const total = count * price;

  const now = new Date();

  records.push({
    name: selectedService.name,
    count: count,
    price: price,
    total: total,
    description: description,
    time: now.toLocaleTimeString("fa-IR", {
      hour: "2-digit",
      minute: "2-digit"
    })
  });

  renderRecords();
  updateSummary();

  selectedService = null;
  serviceSearch.value = "";
  serviceCount.value = 1;
  servicePrice.value = "";
  serviceDescription.value = "";
  selectedServiceName.textContent = "---";
  serviceForm.style.display = "none";
  detailsBox.style.display = "none";
  detailsToggle.textContent = "افزودن توضیحات اختیاری";

  serviceSearch.focus();
}

serviceSearch.addEventListener("input", function() {
  showResults(serviceSearch.value);
});

detailsToggle.addEventListener("click", function() {
  if (detailsBox.style.display === "block") {
    detailsBox.style.display = "none";
    detailsToggle.textContent = "افزودن توضیحات اختیاری";
  } else {
    detailsBox.style.display = "block";
    detailsToggle.textContent = "بستن توضیحات";
  }
});

submitService.addEventListener("click", submitRecord);

updateSummary();
renderRecords();
</script>

</body>
</html>
نمونه اصلی
TEXT - 2026-05-11 23:23:44
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
امتیاز
TEXT - 2026-05-11 23:23:37
<!DOCTYPE html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ثبت خدمات امروز</title> <style> body{ font-family:tahoma; background:#f5f7fb; margin:0; padding:15px; color:#111; } .search-box{ background:#fff; border-radius:14px; padding:10px; box-shadow:0 4px 15px rgba(0,0,0,.08); margin-bottom:12px; } .search-box input{ width:100%; border:none; outline:none; font-size:14px; } .summary{ display:flex; gap:10px; margin-bottom:12px; } .summary-card{ flex:1; background:#fff; padding:12px; border-radius:14px; text-align:center; box-shadow:0 4px 15px rgba(0,0,0,.08); } .summary-title{ font-size:11px; color:#666; margin-bottom:4px; } .summary-value{ font-size:16px; font-weight:bold; } .record-card{ background:#fff7ed; border:1px solid #fed7aa; padding:12px; border-radius:14px; margin-bottom:12px; font-size:13px; } .score-card{ background:#eef2ff; border:1px solid #c7d2fe; padding:12px; border-radius:14px; margin-bottom:12px; } .score-value{ font-size:20px; font-weight:bold; margin-bottom:5px; } .score-positive{ color:#16a34a; } .score-negative{ color:#dc2626; } .form-box{ display:none; background:#fff; border-radius:14px; padding:12px; box-shadow:0 4px 15px rgba(0,0,0,.08); margin-bottom:12px; } .form-box input{ width:100%; padding:8px; margin-bottom:8px; border-radius:8px; border:1px solid #ddd; } button{ width:100%; padding:10px; border:none; border-radius:10px; background:#2563eb; color:#fff; font-size:14px; } .records{ background:#fff; border-radius:14px; padding:12px; box-shadow:0 4px 15px rgba(0,0,0,.08); } .record-item{ border-bottom:1px solid #eee; padding:8px 0; font-size:13px; } .record-item:last-child{ border:none; } </style> </head> <body> <div class="search-box"> <input id="searchInput" placeholder="جستجوی خدمت..."> </div> <div class="summary"> <div class="summary-card"> <div class="summary-title">مبلغ امروز</div> <div class="summary-value" id="todayAmount">۰</div> </div> <div class="summary-card"> <div class="summary-title">تعداد امروز</div> <div class="summary-value" id="todayCount">۰</div> </div> </div> <div class="record-card"> <div id="recordText">هنوز رکوردی ثبت نشده</div> <div id="recordMsg">امروز می‌توانی اولین رکوردت را ثبت کنی</div> </div> <div class="score-card"> <div>امتیاز عملکرد</div> <div id="scoreValue" class="score-value">0</div> <div id="scoreLast">هنوز امتیازی ثبت نشده</div> </div> <div class="form-box" id="serviceForm"> <input id="serviceName" placeholder="نام خدمت"> <input id="serviceCount" type="number" placeholder="تعداد"> <input id="servicePrice" type="number" placeholder="مبلغ هرکدام"> <input id="serviceDesc" placeholder="توضیحات"> <button onclick="addRecord()">ثبت خدمت</button> </div> <div class="records" id="recordsList"></div> <script> let records=[] let score=JSON.parse(localStorage.getItem("workerScore"))||0 let bestRecord=JSON.parse(localStorage.getItem("bestRecord"))||0 const todayAmount=document.getElementById("todayAmount") const todayCount=document.getElementById("todayCount") const recordsList=document.getElementById("recordsList") const recordText=document.getElementById("recordText") const recordMsg=document.getElementById("recordMsg") const scoreValue=document.getElementById("scoreValue") const scoreLast=document.getElementById("scoreLast") const searchInput=document.getElementById("searchInput") const form=document.getElementById("serviceForm") searchInput.addEventListener("input",()=>{ if(searchInput.value.length>1){ form.style.display="block" document.getElementById("serviceName").value=searchInput.value } }) function format(n){ return n.toLocaleString("fa-IR") } function updateSummary(){ let totalAmount=0 let totalCount=0 records.forEach(r=>{ totalAmount+=r.total totalCount+=r.count }) todayAmount.textContent=format(totalAmount) todayCount.textContent=format(totalCount) if(totalCount>bestRecord){ bestRecord=totalCount localStorage.setItem("bestRecord",bestRecord) recordText.textContent="🏆 رکورد جدید: "+format(bestRecord)+" کار" recordMsg.textContent="عالیه! امروز رکورد خودت را شکستی" }else{ if(bestRecord===0){ recordText.textContent="هنوز رکوردی ثبت نشده" }else{ recordText.textContent="بهترین رکورد: "+format(bestRecord)+" کار" let diff=bestRecord-totalCount if(diff>0){ recordMsg.textContent="فقط "+format(diff)+" کار تا رکوردت فاصله داری" } } } } function updateScoreUI(){ scoreValue.textContent=score if(score>0){ scoreValue.className="score-value score-positive" }else if(score<0){ scoreValue.className="score-value score-negative" }else{ scoreValue.className="score-value" } } function addScore(value,reason){ score+=value localStorage.setItem("workerScore",score) let sign=value>0?"+":"" scoreLast.textContent="آخرین امتیاز "+sign+value+" ("+reason+")" updateScoreUI() } function addRecord(){ const name=document.getElementById("serviceName").value const count=parseInt(document.getElementById("serviceCount").value) const price=parseInt(document.getElementById("servicePrice").value) const desc=document.getElementById("serviceDesc").value if(!name||!count||!price)return const total=count*price records.push({ name, count, price, desc, total, time:new Date().toLocaleTimeString("fa-IR") }) renderRecords() updateSummary() addScore(1,"ثبت خدمت "+name) form.reset() } function renderRecords(){ recordsList.innerHTML="" records.slice().reverse().forEach(r=>{ const div=document.createElement("div") div.className="record-item" div.innerHTML=` <strong>${r.name}</strong> <br> تعداد: ${format(r.count)} | مبلغ: ${format(r.total)} <br> ${r.desc||""} ` recordsList.appendChild(div) }) } updateScoreUI() </script> </body> </html>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ثبت خدمات امروز</title>

<style>

body{
font-family:tahoma;
background:#f5f7fb;
margin:0;
padding:15px;
color:#111;
}

.search-box{
background:#fff;
border-radius:14px;
padding:10px;
box-shadow:0 4px 15px rgba(0,0,0,.08);
margin-bottom:12px;
}

.search-box input{
width:100%;
border:none;
outline:none;
font-size:14px;
}

.summary{
display:flex;
gap:10px;
margin-bottom:12px;
}

.summary-card{
flex:1;
background:#fff;
padding:12px;
border-radius:14px;
text-align:center;
box-shadow:0 4px 15px rgba(0,0,0,.08);
}

.summary-title{
font-size:11px;
color:#666;
margin-bottom:4px;
}

.summary-value{
font-size:16px;
font-weight:bold;
}

.record-card{
background:#fff7ed;
border:1px solid #fed7aa;
padding:12px;
border-radius:14px;
margin-bottom:12px;
font-size:13px;
}

.score-card{
background:#eef2ff;
border:1px solid #c7d2fe;
padding:12px;
border-radius:14px;
margin-bottom:12px;
}

.score-value{
font-size:20px;
font-weight:bold;
margin-bottom:5px;
}

.score-positive{
color:#16a34a;
}

.score-negative{
color:#dc2626;
}

.form-box{
display:none;
background:#fff;
border-radius:14px;
padding:12px;
box-shadow:0 4px 15px rgba(0,0,0,.08);
margin-bottom:12px;
}

.form-box input{
width:100%;
padding:8px;
margin-bottom:8px;
border-radius:8px;
border:1px solid #ddd;
}

button{
width:100%;
padding:10px;
border:none;
border-radius:10px;
background:#2563eb;
color:#fff;
font-size:14px;
}

.records{
background:#fff;
border-radius:14px;
padding:12px;
box-shadow:0 4px 15px rgba(0,0,0,.08);
}

.record-item{
border-bottom:1px solid #eee;
padding:8px 0;
font-size:13px;
}

.record-item:last-child{
border:none;
}

</style>
</head>

<body>

<div class="search-box">
<input id="searchInput" placeholder="جستجوی خدمت...">
</div>

<div class="summary">

<div class="summary-card">
<div class="summary-title">مبلغ امروز</div>
<div class="summary-value" id="todayAmount">۰</div>
</div>

<div class="summary-card">
<div class="summary-title">تعداد امروز</div>
<div class="summary-value" id="todayCount">۰</div>
</div>

</div>

<div class="record-card">
<div id="recordText">هنوز رکوردی ثبت نشده</div>
<div id="recordMsg">امروز می‌توانی اولین رکوردت را ثبت کنی</div>
</div>

<div class="score-card">
<div>امتیاز عملکرد</div>
<div id="scoreValue" class="score-value">0</div>
<div id="scoreLast">هنوز امتیازی ثبت نشده</div>
</div>

<div class="form-box" id="serviceForm">

<input id="serviceName" placeholder="نام خدمت">

<input id="serviceCount" type="number" placeholder="تعداد">

<input id="servicePrice" type="number" placeholder="مبلغ هرکدام">

<input id="serviceDesc" placeholder="توضیحات">

<button onclick="addRecord()">ثبت خدمت</button>

</div>

<div class="records" id="recordsList"></div>

<script>

let records=[]
let score=JSON.parse(localStorage.getItem("workerScore"))||0
let bestRecord=JSON.parse(localStorage.getItem("bestRecord"))||0

const todayAmount=document.getElementById("todayAmount")
const todayCount=document.getElementById("todayCount")

const recordsList=document.getElementById("recordsList")

const recordText=document.getElementById("recordText")
const recordMsg=document.getElementById("recordMsg")

const scoreValue=document.getElementById("scoreValue")
const scoreLast=document.getElementById("scoreLast")

const searchInput=document.getElementById("searchInput")
const form=document.getElementById("serviceForm")

searchInput.addEventListener("input",()=>{
if(searchInput.value.length>1){
form.style.display="block"
document.getElementById("serviceName").value=searchInput.value
}
})

function format(n){
return n.toLocaleString("fa-IR")
}

function updateSummary(){

let totalAmount=0
let totalCount=0

records.forEach(r=>{
totalAmount+=r.total
totalCount+=r.count
})

todayAmount.textContent=format(totalAmount)
todayCount.textContent=format(totalCount)

if(totalCount>bestRecord){

bestRecord=totalCount
localStorage.setItem("bestRecord",bestRecord)

recordText.textContent="🏆 رکورد جدید: "+format(bestRecord)+" کار"
recordMsg.textContent="عالیه! امروز رکورد خودت را شکستی"

}else{

if(bestRecord===0){

recordText.textContent="هنوز رکوردی ثبت نشده"

}else{

recordText.textContent="بهترین رکورد: "+format(bestRecord)+" کار"

let diff=bestRecord-totalCount

if(diff>0){
recordMsg.textContent="فقط "+format(diff)+" کار تا رکوردت فاصله داری"
}

}

}

}

function updateScoreUI(){

scoreValue.textContent=score

if(score>0){
scoreValue.className="score-value score-positive"
}else if(score<0){
scoreValue.className="score-value score-negative"
}else{
scoreValue.className="score-value"
}

}

function addScore(value,reason){

score+=value

localStorage.setItem("workerScore",score)

let sign=value>0?"+":""

scoreLast.textContent="آخرین امتیاز "+sign+value+" ("+reason+")"

updateScoreUI()

}

function addRecord(){

const name=document.getElementById("serviceName").value
const count=parseInt(document.getElementById("serviceCount").value)
const price=parseInt(document.getElementById("servicePrice").value)
const desc=document.getElementById("serviceDesc").value

if(!name||!count||!price)return

const total=count*price

records.push({
name,
count,
price,
desc,
total,
time:new Date().toLocaleTimeString("fa-IR")
})

renderRecords()

updateSummary()

addScore(1,"ثبت خدمت "+name)

form.reset()

}

function renderRecords(){

recordsList.innerHTML=""

records.slice().reverse().forEach(r=>{

const div=document.createElement("div")
div.className="record-item"

div.innerHTML=`
<strong>${r.name}</strong>
<br>
تعداد: ${format(r.count)} | مبلغ: ${format(r.total)}
<br>
${r.desc||""}
`

recordsList.appendChild(div)

})

}

updateScoreUI()

</script>

</body>
</html>
نمونه اصلی
TEXT - 2026-05-11 23:08:31
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
جاپا ۲
TEXT - 2026-05-11 23:08:14
<div class="worker-page" dir="rtl"> <div class="worker-mobile"> <!-- Header --> <div class="worker-header"> <div> <h1>ثبت کارکرد</h1> <p>سلام، عرفان</p> </div> <div class="date-badge">امروز</div> </div> <!-- Summary --> <div class="summary-wrap"> <div class="big-money-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="side-summary"> <div class="mini-card blue"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> <div class="status-box"> <div class="status-row"> <span>در انتظار تایید</span> <strong id="pendingCount">۰</strong> </div> <div class="status-row approved-row"> <span>تایید شده</span> <strong id="approvedAmount">۰ تومان</strong> </div> </div> </div> </div> <!-- Last Record --> <div class="info-strip single"> <div class="info-item"> <span>آخرین ثبت</span> <strong id="lastRecordTitle">هنوز چیزی ثبت نشده</strong> <small id="lastRecordTime">-</small> </div> </div> <!-- Search --> <div class="search-highlight"> <label>جستجوی خدمت</label> <input type="text" id="serviceSearch" placeholder="مثلاً: میز لبه‌دار ۳۵" /> <div class="search-results" id="searchResults"></div> </div> <!-- Selected Service Form --> <div class="service-form-wrap hidden" id="serviceFormWrap"> <div class="section-head"> <span>خدمت انتخاب‌شده</span> <small>اطلاعات کار را وارد کن</small> </div> <div class="service-form-card"> <div class="service-top"> <div> <h3 id="selectedServiceName">-</h3> <p id="selectedServicePriceText">قیمت واحد: -</p> </div> <div class="price-tag" id="selectedServicePriceTag">-</div> </div> <div class="optional-note-toggle" id="toggleDescription"> + افزودن توضیحات </div> <div class="description-box hidden" id="descriptionBox"> <div class="form-group"> <label>توضیحات</label> <textarea id="descriptionInput" placeholder="مثلاً رنگ، مدل، سفارش خاص، ایراد یا نکته..."></textarea> </div> </div> <div class="form-row"> <div class="form-group"> <label>تعداد</label> <input type="number" id="countInput" value="1" min="1" /> </div> <div class="form-group"> <label>مقدار</label> <input type="text" id="amountInput" placeholder="مثلاً ۱۲ متر" /> </div> </div> <button type="button" class="submit-btn" id="submitWorkBtn">ثبت کارکرد</button> </div> </div> <!-- Today Records --> <div class="section-head"> <span>ثبت‌های امروز</span> <small>آخرین کارهای ثبت‌شده</small> </div> <div class="today-list" id="todayList"> <div class="empty-state" id="emptyState">هنوز کاری ثبت نشده است.</div> </div> </div> </div> <style> .worker-page { min-height: 100vh; background: #e5e7eb; display: flex; justify-content: center; padding: 18px 0; box-sizing: border-box; font-family: Tahoma, Arial, sans-serif; } .worker-mobile { width: 100%; max-width: 430px; min-height: 100vh; background: #f8fafc; border-radius: 28px; padding: 16px 14px 30px; box-sizing: border-box; } .hidden { display: none !important; } .worker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; } .worker-header h1 { margin: 0; font-size: 23px; font-weight: 900; color: #0f172a; } .worker-header p { margin: 6px 0 0; font-size: 13px; color: #64748b; } .date-badge { background: #111827; color: #fff; padding: 8px 13px; border-radius: 999px; font-size: 12px; font-weight: 800; } .summary-wrap { display: grid; grid-template-columns: 1.25fr .95fr; gap: 10px; margin-bottom: 14px; } .big-money-card { background: linear-gradient(135deg, #16a34a, #22c55e); color: #fff; border-radius: 22px; padding: 16px 15px; min-height: 118px; display: flex; flex-direction: column; justify-content: center; box-sizing: border-box; box-shadow: 0 10px 24px rgba(34, 197, 94, .18); } .big-money-card span { font-size: 13px; font-weight: 800; margin-bottom: 8px; opacity: .95; } .big-money-card strong { font-size: 28px; line-height: 1.4; font-weight: 900; } .side-summary { display: flex; flex-direction: column; gap: 10px; } .mini-card { border-radius: 18px; padding: 12px 10px; min-height: 54px; color: #fff; text-align: center; display: flex; flex-direction: column; justify-content: center; } .mini-card span { font-size: 11px; font-weight: 800; margin-bottom: 4px; } .mini-card strong { font-size: 18px; font-weight: 900; } .mini-card.blue { background: linear-gradient(135deg, #2563eb, #3b82f6); } .status-box { background: #fff; border-radius: 18px; padding: 10px; box-shadow: 0 8px 20px rgba(15, 23, 42, .06); } .status-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 2px; color: #334155; font-size: 12px; font-weight: 800; } .status-row strong { color: #0f172a; font-size: 13px; font-weight: 900; } .approved-row { border-top: 1px solid #e2e8f0; margin-top: 2px; padding-top: 10px; } .info-strip.single { margin-bottom: 16px; } .info-item { background: #fff; border-radius: 17px; padding: 11px 12px; box-shadow: 0 7px 18px rgba(15, 23, 42, .05); box-sizing: border-box; } .info-item span { display: block; color: #64748b; font-size: 11px; font-weight: 800; margin-bottom: 5px; } .info-item strong { display: block; color: #0f172a; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .info-item small { color: #94a3b8; font-size: 11px; } .search-highlight { background: linear-gradient(135deg, #dbeafe, #eff6ff); border: 2px solid #93c5fd; border-radius: 22px; padding: 14px; margin-bottom: 18px; box-shadow: 0 10px 24px rgba(37, 99, 235, .12); } .search-highlight label { display: block; margin-bottom: 9px; color: #1d4ed8; font-size: 13px; font-weight: 900; } .search-highlight input { width: 100%; height: 56px; border: none; outline: none; border-radius: 16px; background: #fff; padding: 0 15px; box-sizing: border-box; font-size: 15px; font-weight: 700; color: #111827; } .search-results { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; } .result-item { background: #fff; border-radius: 14px; padding: 10px 12px; cursor: pointer; border: 1px solid #dbeafe; } .result-item strong { display: block; font-size: 13px; color: #0f172a; margin-bottom: 4px; } .result-item small { color: #64748b; font-size: 11px; } .result-item:hover { background: #eff6ff; } .service-form-wrap { margin-bottom: 10px; } .section-head { display: flex; justify-content: space-between; align-items: center; margin: 15px 2px 9px; } .section-head span { font-size: 13px; color: #334155; font-weight: 900; } .section-head small { color: #94a3b8; font-size: 11px; } .service-form-card { background: #fff; border-radius: 22px; padding: 15px; box-shadow: 0 10px 28px rgba(15, 23, 42, .07); box-sizing: border-box; } .service-top { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; margin-bottom: 13px; } .service-top h3 { margin: 0 0 6px; font-size: 16px; color: #111827; font-weight: 900; } .service-top p { margin: 0; color: #64748b; font-size: 12px; } .price-tag { background: #eff6ff; color: #2563eb; border-radius: 999px; padding: 8px 10px; font-size: 12px; font-weight: 900; white-space: nowrap; } .optional-note-toggle { background: #f8fafc; border: 1px dashed #cbd5e1; color: #334155; border-radius: 14px; padding: 12px 14px; font-size: 13px; font-weight: 800; margin-bottom: 12px; cursor: pointer; } .description-box { margin-bottom: 8px; } .form-group { margin-bottom: 11px; } .form-group label { display: block; font-size: 12px; font-weight: 900; color: #475569; margin-bottom: 7px; } .form-group input, .form-group textarea { width: 100%; border: none; outline: none; background: #f8fafc; border-radius: 15px; box-sizing: border-box; font-family: inherit; color: #111827; } .form-group input { height: 48px; padding: 0 13px; font-size: 15px; font-weight: 800; } .form-group textarea { min-height: 78px; resize: none; padding: 12px 13px; font-size: 13px; line-height: 1.8; } .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .submit-btn { width: 100%; height: 52px; border: none; border-radius: 17px; background: #16a34a; color: #fff; font-size: 15px; font-weight: 900; margin-top: 2px; cursor: pointer; } .today-list { display: flex; flex-direction: column; gap: 9px; } .today-item { background: #fff; border-radius: 18px; padding: 13px; box-shadow: 0 7px 18px rgba(15, 23, 42, .05); } .today-item h4 { margin: 0 0 7px; color: #111827; font-size: 14px; font-weight: 900; } .today-item p { margin: 0 0 4px; color: #64748b; font-size: 12px; line-height: 1.7; } .item-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 9px; } .amount { color: #111827; font-size: 13px; font-weight: 900; } .status { border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 900; } .status.pending { background: #fef3c7; color: #92400e; } .status.approved { background: #dcfce7; color: #15803d; } .empty-state { background: #fff; border-radius: 16px; padding: 16px; text-align: center; color: #94a3b8; font-size: 13px; font-weight: 700; } @media (max-width: 390px) { .summary-wrap { grid-template-columns: 1fr; } .big-money-card strong { font-size: 24px; } .section-head small { display: none; } } </style> <script> const services = [ { id: 1, name: 'میز لبه‌دار ۳۵', price: 95000 }, { id: 2, name: 'میز لبه‌دار ۵۰', price: 110000 }, { id: 3, name: 'میز خام', price: 80000 }, { id: 4, name: 'برش MDF', price: 65000 }, { id: 5, name: 'مونتاژ کمد', price: 140000 }, { id: 6, name: 'لبه چسبانی', price: 70000 } ]; let selectedService = null; let records = []; const serviceSearch = document.getElementById('serviceSearch'); const searchResults = document.getElementById('searchResults'); const serviceFormWrap = document.getElementById('serviceFormWrap'); const selectedServiceName = document.getElementById('selectedServiceName'); const selectedServicePriceText = document.getElementById('selectedServicePriceText'); const selectedServicePriceTag = document.getElementById('selectedServicePriceTag'); const toggleDescription = document.getElementById('toggleDescription'); const descriptionBox = document.getElementById('descriptionBox'); const descriptionInput = document.getElementById('descriptionInput'); const countInput = document.getElementById('countInput'); const amountInput = document.getElementById('amountInput'); const submitWorkBtn = document.getElementById('submitWorkBtn'); const todayList = document.getElementById('todayList'); const emptyState = document.getElementById('emptyState'); const todayAmount = document.getElementById('todayAmount'); const todayCount = document.getElementById('todayCount'); const pendingCount = document.getElementById('pendingCount'); const approvedAmount = document.getElementById('approvedAmount'); const lastRecordTitle = document.getElementById('lastRecordTitle'); const lastRecordTime = document.getElementById('lastRecordTime'); function formatToman(number) { return Number(number).toLocaleString('fa-IR') + ' تومان'; } function formatNumber(number) { return Number(number).toLocaleString('fa-IR'); } function renderSearchResults(list) { searchResults.innerHTML = ''; if (!list.length) return; list.forEach(service => { const item = document.createElement('div'); item.className = 'result-item'; item.innerHTML = ` <strong>${service.name}</strong> <small>قیمت واحد: ${formatToman(service.price)}</small> `; item.addEventListener('click', function () { selectService(service); }); searchResults.appendChild(item); }); } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; selectedServicePriceText.textContent = 'قیمت واحد: ' + formatToman(service.price); selectedServicePriceTag.textContent = formatNumber(service.price); serviceSearch.value = service.name; searchResults.innerHTML = ''; serviceFormWrap.classList.remove('hidden'); descriptionBox.classList.add('hidden'); toggleDescription.textContent = '+ افزودن توضیحات'; countInput.value = 1; amountInput.value = ''; descriptionInput.value = ''; setTimeout(() => { countInput.focus(); }, 100); } serviceSearch.addEventListener('input', function () { const value = this.value.trim(); if (!value) { searchResults.innerHTML = ''; serviceFormWrap.classList.add('hidden'); selectedService = null; return; } const filtered = services.filter(service => service.name.includes(value)); renderSearchResults(filtered); }); toggleDescription.addEventListener('click', function () { descriptionBox.classList.toggle('hidden'); if (descriptionBox.classList.contains('hidden')) { toggleDescription.textContent = '+ افزودن توضیحات'; } else { toggleDescription.textContent = '- بستن توضیحات'; descriptionInput.focus(); } }); function getNowText() { const now = new Date(); return now.toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }); } function updateSummary() { const totalAmount = records.reduce((sum, item) => sum + item.total, 0); const totalCount = records.reduce((sum, item) => sum + item.count, 0); const pending = records.filter(item => item.status === 'pending').length; const approved = records .filter(item => item.status === 'approved') .reduce((sum, item) => sum + item.total, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = formatNumber(totalCount); pendingCount.textContent = formatNumber(pending); approvedAmount.textContent = formatToman(approved); } function renderRecords() { todayList.innerHTML = ''; if (!records.length) { todayList.appendChild(emptyState); return; } records.slice().reverse().forEach(record => { const item = document.createElement('div'); item.className = 'today-item'; item.innerHTML = ` <div class="item-main"> <h4>${record.name}</h4> <p>تعداد: ${formatNumber(record.count)} | مقدار: ${record.amount || '-'}</p> <p>توضیح: ${record.description || '-'}</p> <div class="item-footer"> <span class="amount">${formatToman(record.total)}</span> <span class="status ${record.status}"> ${record.status === 'pending' ? 'در انتظار تایید' : 'تایید شده'} </span> </div> </div> `; item.addEventListener('dblclick', function () { if (record.status === 'pending') { record.status = 'approved'; renderRecords(); updateSummary(); } }); todayList.appendChild(item); }); } submitWorkBtn.addEventListener('click', function () { if (!selectedService) { alert('ابتدا یک خدمت را از جستجو انتخاب کنید.'); return; } const count = parseInt(countInput.value, 10); const amount = amountInput.value.trim(); const description = descriptionInput.value.trim(); if (!count || count < 1) { alert('تعداد معتبر وارد کنید.'); return; } const total = count * selectedService.price; const record = { name: selectedService.name, price: selectedService.price, count: count, amount: amount, description: description, total: total, status: 'pending', time: getNowText() }; records.push(record); lastRecordTitle.textContent = record.name; lastRecordTime.textContent = 'ثبت در ساعت ' + record.time; renderRecords(); updateSummary(); serviceSearch.value = ''; selectedService = null; serviceFormWrap.classList.add('hidden'); countInput.value = 1; amountInput.value = ''; descriptionInput.value = ''; descriptionBox.classList.add('hidden'); toggleDescription.textContent = '+ افزودن توضیحات'; alert('کارکرد با موفقیت ثبت شد.'); }); updateSummary(); renderRecords(); </script>
<div class="worker-page" dir="rtl">
  <div class="worker-mobile">

    <!-- Header -->
    <div class="worker-header">
      <div>
        <h1>ثبت کارکرد</h1>
        <p>سلام، عرفان</p>
      </div>
      <div class="date-badge">امروز</div>
    </div>

    <!-- Summary -->
    <div class="summary-wrap">
      <div class="big-money-card">
        <span>مبلغ امروز</span>
        <strong id="todayAmount">۰ تومان</strong>
      </div>

      <div class="side-summary">
        <div class="mini-card blue">
          <span>تعداد امروز</span>
          <strong id="todayCount">۰</strong>
        </div>

        <div class="status-box">
          <div class="status-row">
            <span>در انتظار تایید</span>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="status-row approved-row">
            <span>تایید شده</span>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
        </div>
      </div>
    </div>

    <!-- Last Record -->
    <div class="info-strip single">
      <div class="info-item">
        <span>آخرین ثبت</span>
        <strong id="lastRecordTitle">هنوز چیزی ثبت نشده</strong>
        <small id="lastRecordTime">-</small>
      </div>
    </div>

    <!-- Search -->
    <div class="search-highlight">
      <label>جستجوی خدمت</label>
      <input type="text" id="serviceSearch" placeholder="مثلاً: میز لبه‌دار ۳۵" />
      <div class="search-results" id="searchResults"></div>
    </div>

    <!-- Selected Service Form -->
    <div class="service-form-wrap hidden" id="serviceFormWrap">
      <div class="section-head">
        <span>خدمت انتخاب‌شده</span>
        <small>اطلاعات کار را وارد کن</small>
      </div>

      <div class="service-form-card">
        <div class="service-top">
          <div>
            <h3 id="selectedServiceName">-</h3>
            <p id="selectedServicePriceText">قیمت واحد: -</p>
          </div>
          <div class="price-tag" id="selectedServicePriceTag">-</div>
        </div>

        <div class="optional-note-toggle" id="toggleDescription">
          + افزودن توضیحات
        </div>

        <div class="description-box hidden" id="descriptionBox">
          <div class="form-group">
            <label>توضیحات</label>
            <textarea id="descriptionInput" placeholder="مثلاً رنگ، مدل، سفارش خاص، ایراد یا نکته..."></textarea>
          </div>
        </div>

        <div class="form-row">
          <div class="form-group">
            <label>تعداد</label>
            <input type="number" id="countInput" value="1" min="1" />
          </div>

          <div class="form-group">
            <label>مقدار</label>
            <input type="text" id="amountInput" placeholder="مثلاً ۱۲ متر" />
          </div>
        </div>

        <button type="button" class="submit-btn" id="submitWorkBtn">ثبت کارکرد</button>
      </div>
    </div>

    <!-- Today Records -->
    <div class="section-head">
      <span>ثبت‌های امروز</span>
      <small>آخرین کارهای ثبت‌شده</small>
    </div>

    <div class="today-list" id="todayList">
      <div class="empty-state" id="emptyState">هنوز کاری ثبت نشده است.</div>
    </div>

  </div>
</div>

<style>
  .worker-page {
    min-height: 100vh;
    background: #e5e7eb;
    display: flex;
    justify-content: center;
    padding: 18px 0;
    box-sizing: border-box;
    font-family: Tahoma, Arial, sans-serif;
  }

  .worker-mobile {
    width: 100%;
    max-width: 430px;
    min-height: 100vh;
    background: #f8fafc;
    border-radius: 28px;
    padding: 16px 14px 30px;
    box-sizing: border-box;
  }

  .hidden {
    display: none !important;
  }

  .worker-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 14px;
  }

  .worker-header h1 {
    margin: 0;
    font-size: 23px;
    font-weight: 900;
    color: #0f172a;
  }

  .worker-header p {
    margin: 6px 0 0;
    font-size: 13px;
    color: #64748b;
  }

  .date-badge {
    background: #111827;
    color: #fff;
    padding: 8px 13px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 800;
  }

  .summary-wrap {
    display: grid;
    grid-template-columns: 1.25fr .95fr;
    gap: 10px;
    margin-bottom: 14px;
  }

  .big-money-card {
    background: linear-gradient(135deg, #16a34a, #22c55e);
    color: #fff;
    border-radius: 22px;
    padding: 16px 15px;
    min-height: 118px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    box-sizing: border-box;
    box-shadow: 0 10px 24px rgba(34, 197, 94, .18);
  }

  .big-money-card span {
    font-size: 13px;
    font-weight: 800;
    margin-bottom: 8px;
    opacity: .95;
  }

  .big-money-card strong {
    font-size: 28px;
    line-height: 1.4;
    font-weight: 900;
  }

  .side-summary {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .mini-card {
    border-radius: 18px;
    padding: 12px 10px;
    min-height: 54px;
    color: #fff;
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .mini-card span {
    font-size: 11px;
    font-weight: 800;
    margin-bottom: 4px;
  }

  .mini-card strong {
    font-size: 18px;
    font-weight: 900;
  }

  .mini-card.blue {
    background: linear-gradient(135deg, #2563eb, #3b82f6);
  }

  .status-box {
    background: #fff;
    border-radius: 18px;
    padding: 10px;
    box-shadow: 0 8px 20px rgba(15, 23, 42, .06);
  }

  .status-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 2px;
    color: #334155;
    font-size: 12px;
    font-weight: 800;
  }

  .status-row strong {
    color: #0f172a;
    font-size: 13px;
    font-weight: 900;
  }

  .approved-row {
    border-top: 1px solid #e2e8f0;
    margin-top: 2px;
    padding-top: 10px;
  }

  .info-strip.single {
    margin-bottom: 16px;
  }

  .info-item {
    background: #fff;
    border-radius: 17px;
    padding: 11px 12px;
    box-shadow: 0 7px 18px rgba(15, 23, 42, .05);
    box-sizing: border-box;
  }

  .info-item span {
    display: block;
    color: #64748b;
    font-size: 11px;
    font-weight: 800;
    margin-bottom: 5px;
  }

  .info-item strong {
    display: block;
    color: #0f172a;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .info-item small {
    color: #94a3b8;
    font-size: 11px;
  }

  .search-highlight {
    background: linear-gradient(135deg, #dbeafe, #eff6ff);
    border: 2px solid #93c5fd;
    border-radius: 22px;
    padding: 14px;
    margin-bottom: 18px;
    box-shadow: 0 10px 24px rgba(37, 99, 235, .12);
  }

  .search-highlight label {
    display: block;
    margin-bottom: 9px;
    color: #1d4ed8;
    font-size: 13px;
    font-weight: 900;
  }

  .search-highlight input {
    width: 100%;
    height: 56px;
    border: none;
    outline: none;
    border-radius: 16px;
    background: #fff;
    padding: 0 15px;
    box-sizing: border-box;
    font-size: 15px;
    font-weight: 700;
    color: #111827;
  }

  .search-results {
    margin-top: 10px;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .result-item {
    background: #fff;
    border-radius: 14px;
    padding: 10px 12px;
    cursor: pointer;
    border: 1px solid #dbeafe;
  }

  .result-item strong {
    display: block;
    font-size: 13px;
    color: #0f172a;
    margin-bottom: 4px;
  }

  .result-item small {
    color: #64748b;
    font-size: 11px;
  }

  .result-item:hover {
    background: #eff6ff;
  }

  .service-form-wrap {
    margin-bottom: 10px;
  }

  .section-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 15px 2px 9px;
  }

  .section-head span {
    font-size: 13px;
    color: #334155;
    font-weight: 900;
  }

  .section-head small {
    color: #94a3b8;
    font-size: 11px;
  }

  .service-form-card {
    background: #fff;
    border-radius: 22px;
    padding: 15px;
    box-shadow: 0 10px 28px rgba(15, 23, 42, .07);
    box-sizing: border-box;
  }

  .service-top {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    align-items: flex-start;
    margin-bottom: 13px;
  }

  .service-top h3 {
    margin: 0 0 6px;
    font-size: 16px;
    color: #111827;
    font-weight: 900;
  }

  .service-top p {
    margin: 0;
    color: #64748b;
    font-size: 12px;
  }

  .price-tag {
    background: #eff6ff;
    color: #2563eb;
    border-radius: 999px;
    padding: 8px 10px;
    font-size: 12px;
    font-weight: 900;
    white-space: nowrap;
  }

  .optional-note-toggle {
    background: #f8fafc;
    border: 1px dashed #cbd5e1;
    color: #334155;
    border-radius: 14px;
    padding: 12px 14px;
    font-size: 13px;
    font-weight: 800;
    margin-bottom: 12px;
    cursor: pointer;
  }

  .description-box {
    margin-bottom: 8px;
  }

  .form-group {
    margin-bottom: 11px;
  }

  .form-group label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    color: #475569;
    margin-bottom: 7px;
  }

  .form-group input,
  .form-group textarea {
    width: 100%;
    border: none;
    outline: none;
    background: #f8fafc;
    border-radius: 15px;
    box-sizing: border-box;
    font-family: inherit;
    color: #111827;
  }

  .form-group input {
    height: 48px;
    padding: 0 13px;
    font-size: 15px;
    font-weight: 800;
  }

  .form-group textarea {
    min-height: 78px;
    resize: none;
    padding: 12px 13px;
    font-size: 13px;
    line-height: 1.8;
  }

  .form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .submit-btn {
    width: 100%;
    height: 52px;
    border: none;
    border-radius: 17px;
    background: #16a34a;
    color: #fff;
    font-size: 15px;
    font-weight: 900;
    margin-top: 2px;
    cursor: pointer;
  }

  .today-list {
    display: flex;
    flex-direction: column;
    gap: 9px;
  }

  .today-item {
    background: #fff;
    border-radius: 18px;
    padding: 13px;
    box-shadow: 0 7px 18px rgba(15, 23, 42, .05);
  }

  .today-item h4 {
    margin: 0 0 7px;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .today-item p {
    margin: 0 0 4px;
    color: #64748b;
    font-size: 12px;
    line-height: 1.7;
  }

  .item-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 9px;
  }

  .amount {
    color: #111827;
    font-size: 13px;
    font-weight: 900;
  }

  .status {
    border-radius: 999px;
    padding: 5px 10px;
    font-size: 11px;
    font-weight: 900;
  }

  .status.pending {
    background: #fef3c7;
    color: #92400e;
  }

  .status.approved {
    background: #dcfce7;
    color: #15803d;
  }

  .empty-state {
    background: #fff;
    border-radius: 16px;
    padding: 16px;
    text-align: center;
    color: #94a3b8;
    font-size: 13px;
    font-weight: 700;
  }

  @media (max-width: 390px) {
    .summary-wrap {
      grid-template-columns: 1fr;
    }

    .big-money-card strong {
      font-size: 24px;
    }

    .section-head small {
      display: none;
    }
  }
</style>

<script>
  const services = [
    { id: 1, name: 'میز لبه‌دار ۳۵', price: 95000 },
    { id: 2, name: 'میز لبه‌دار ۵۰', price: 110000 },
    { id: 3, name: 'میز خام', price: 80000 },
    { id: 4, name: 'برش MDF', price: 65000 },
    { id: 5, name: 'مونتاژ کمد', price: 140000 },
    { id: 6, name: 'لبه چسبانی', price: 70000 }
  ];

  let selectedService = null;
  let records = [];

  const serviceSearch = document.getElementById('serviceSearch');
  const searchResults = document.getElementById('searchResults');
  const serviceFormWrap = document.getElementById('serviceFormWrap');

  const selectedServiceName = document.getElementById('selectedServiceName');
  const selectedServicePriceText = document.getElementById('selectedServicePriceText');
  const selectedServicePriceTag = document.getElementById('selectedServicePriceTag');

  const toggleDescription = document.getElementById('toggleDescription');
  const descriptionBox = document.getElementById('descriptionBox');
  const descriptionInput = document.getElementById('descriptionInput');

  const countInput = document.getElementById('countInput');
  const amountInput = document.getElementById('amountInput');
  const submitWorkBtn = document.getElementById('submitWorkBtn');

  const todayList = document.getElementById('todayList');
  const emptyState = document.getElementById('emptyState');

  const todayAmount = document.getElementById('todayAmount');
  const todayCount = document.getElementById('todayCount');
  const pendingCount = document.getElementById('pendingCount');
  const approvedAmount = document.getElementById('approvedAmount');
  const lastRecordTitle = document.getElementById('lastRecordTitle');
  const lastRecordTime = document.getElementById('lastRecordTime');

  function formatToman(number) {
    return Number(number).toLocaleString('fa-IR') + ' تومان';
  }

  function formatNumber(number) {
    return Number(number).toLocaleString('fa-IR');
  }

  function renderSearchResults(list) {
    searchResults.innerHTML = '';

    if (!list.length) return;

    list.forEach(service => {
      const item = document.createElement('div');
      item.className = 'result-item';
      item.innerHTML = `
        <strong>${service.name}</strong>
        <small>قیمت واحد: ${formatToman(service.price)}</small>
      `;

      item.addEventListener('click', function () {
        selectService(service);
      });

      searchResults.appendChild(item);
    });
  }

  function selectService(service) {
    selectedService = service;

    selectedServiceName.textContent = service.name;
    selectedServicePriceText.textContent = 'قیمت واحد: ' + formatToman(service.price);
    selectedServicePriceTag.textContent = formatNumber(service.price);

    serviceSearch.value = service.name;
    searchResults.innerHTML = '';

    serviceFormWrap.classList.remove('hidden');

    descriptionBox.classList.add('hidden');
    toggleDescription.textContent = '+ افزودن توضیحات';

    countInput.value = 1;
    amountInput.value = '';
    descriptionInput.value = '';

    setTimeout(() => {
      countInput.focus();
    }, 100);
  }

  serviceSearch.addEventListener('input', function () {
    const value = this.value.trim();

    if (!value) {
      searchResults.innerHTML = '';
      serviceFormWrap.classList.add('hidden');
      selectedService = null;
      return;
    }

    const filtered = services.filter(service => service.name.includes(value));
    renderSearchResults(filtered);
  });

  toggleDescription.addEventListener('click', function () {
    descriptionBox.classList.toggle('hidden');

    if (descriptionBox.classList.contains('hidden')) {
      toggleDescription.textContent = '+ افزودن توضیحات';
    } else {
      toggleDescription.textContent = '- بستن توضیحات';
      descriptionInput.focus();
    }
  });

  function getNowText() {
    const now = new Date();
    return now.toLocaleTimeString('fa-IR', {
      hour: '2-digit',
      minute: '2-digit'
    });
  }

  function updateSummary() {
    const totalAmount = records.reduce((sum, item) => sum + item.total, 0);
    const totalCount = records.reduce((sum, item) => sum + item.count, 0);
    const pending = records.filter(item => item.status === 'pending').length;
    const approved = records
      .filter(item => item.status === 'approved')
      .reduce((sum, item) => sum + item.total, 0);

    todayAmount.textContent = formatToman(totalAmount);
    todayCount.textContent = formatNumber(totalCount);
    pendingCount.textContent = formatNumber(pending);
    approvedAmount.textContent = formatToman(approved);
  }

  function renderRecords() {
    todayList.innerHTML = '';

    if (!records.length) {
      todayList.appendChild(emptyState);
      return;
    }

    records.slice().reverse().forEach(record => {
      const item = document.createElement('div');
      item.className = 'today-item';

      item.innerHTML = `
        <div class="item-main">
          <h4>${record.name}</h4>
          <p>تعداد: ${formatNumber(record.count)} | مقدار: ${record.amount || '-'}</p>
          <p>توضیح: ${record.description || '-'}</p>
          <div class="item-footer">
            <span class="amount">${formatToman(record.total)}</span>
            <span class="status ${record.status}">
              ${record.status === 'pending' ? 'در انتظار تایید' : 'تایید شده'}
            </span>
          </div>
        </div>
      `;

      item.addEventListener('dblclick', function () {
        if (record.status === 'pending') {
          record.status = 'approved';
          renderRecords();
          updateSummary();
        }
      });

      todayList.appendChild(item);
    });
  }

  submitWorkBtn.addEventListener('click', function () {
    if (!selectedService) {
      alert('ابتدا یک خدمت را از جستجو انتخاب کنید.');
      return;
    }

    const count = parseInt(countInput.value, 10);
    const amount = amountInput.value.trim();
    const description = descriptionInput.value.trim();

    if (!count || count < 1) {
      alert('تعداد معتبر وارد کنید.');
      return;
    }

    const total = count * selectedService.price;

    const record = {
      name: selectedService.name,
      price: selectedService.price,
      count: count,
      amount: amount,
      description: description,
      total: total,
      status: 'pending',
      time: getNowText()
    };

    records.push(record);

    lastRecordTitle.textContent = record.name;
    lastRecordTime.textContent = 'ثبت در ساعت ' + record.time;

    renderRecords();
    updateSummary();

    serviceSearch.value = '';
    selectedService = null;
    serviceFormWrap.classList.add('hidden');

    countInput.value = 1;
    amountInput.value = '';
    descriptionInput.value = '';
    descriptionBox.classList.add('hidden');
    toggleDescription.textContent = '+ افزودن توضیحات';

    alert('کارکرد با موفقیت ثبت شد.');
  });

  updateSummary();
  renderRecords();
</script>
نمونه اصلی
TEXT - 2026-05-11 23:02:53
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
با جاوا
TEXT - 2026-05-11 23:02:39
<div class="worker-page" dir="rtl"> <div class="worker-mobile"> <!-- Header --> <div class="worker-header"> <div> <h1>ثبت کارکرد</h1> <p>سلام، عرفان</p> </div> <div class="date-badge">امروز</div> </div> <!-- Summary --> <div class="summary-wrap"> <div class="big-money-card"> <span>مبلغ امروز</span> <strong id="todayAmount">۰ تومان</strong> </div> <div class="side-summary"> <div class="mini-card blue"> <span>تعداد امروز</span> <strong id="todayCount">۰</strong> </div> <div class="status-box"> <div class="status-row"> <span>در انتظار تایید</span> <strong id="pendingCount">۰</strong> </div> <div class="status-row approved-row"> <span>تایید شده</span> <strong id="approvedAmount">۰ تومان</strong> </div> </div> </div> </div> <!-- Last Record --> <div class="info-strip single"> <div class="info-item"> <span>آخرین ثبت</span> <strong id="lastRecordTitle">هنوز چیزی ثبت نشده</strong> <small id="lastRecordTime">-</small> </div> </div> <!-- Search --> <div class="search-highlight"> <label>جستجوی خدمت</label> <input type="text" id="serviceSearch" placeholder="مثلاً: میز لبه‌دار ۳۵" /> <div class="search-results" id="searchResults"></div> </div> <!-- Selected Service Form --> <div class="section-head"> <span>خدمت انتخاب‌شده</span> <small>اطلاعات کار را وارد کن</small> </div> <div class="service-form-card"> <div class="service-top"> <div> <h3 id="selectedServiceName">هنوز خدمتی انتخاب نشده</h3> <p id="selectedServicePriceText">قیمت واحد: -</p> </div> <div class="price-tag" id="selectedServicePriceTag">-</div> </div> <div class="form-group"> <label>توضیحات</label> <textarea id="descriptionInput" placeholder="مثلاً رنگ، مدل، سفارش خاص، ایراد یا نکته..."></textarea> </div> <div class="form-row"> <div class="form-group"> <label>تعداد</label> <input type="number" id="countInput" value="1" min="1" /> </div> <div class="form-group"> <label>مقدار</label> <input type="text" id="amountInput" placeholder="مثلاً ۱۲ متر" /> </div> </div> <button type="button" class="submit-btn" id="submitWorkBtn">ثبت کارکرد</button> </div> <!-- Today Records --> <div class="section-head"> <span>ثبت‌های امروز</span> <small>آخرین کارهای ثبت‌شده</small> </div> <div class="today-list" id="todayList"> <div class="empty-state" id="emptyState">هنوز کاری ثبت نشده است.</div> </div> </div> </div> <style> .worker-page { min-height: 100vh; background: #e5e7eb; display: flex; justify-content: center; padding: 18px 0; box-sizing: border-box; font-family: Tahoma, Arial, sans-serif; } .worker-mobile { width: 100%; max-width: 430px; min-height: 100vh; background: #f8fafc; border-radius: 28px; padding: 16px 14px 30px; box-sizing: border-box; } .worker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; } .worker-header h1 { margin: 0; font-size: 23px; font-weight: 900; color: #0f172a; } .worker-header p { margin: 6px 0 0; font-size: 13px; color: #64748b; } .date-badge { background: #111827; color: #fff; padding: 8px 13px; border-radius: 999px; font-size: 12px; font-weight: 800; } .summary-wrap { display: grid; grid-template-columns: 1.25fr .95fr; gap: 10px; margin-bottom: 14px; } .big-money-card { background: linear-gradient(135deg, #16a34a, #22c55e); color: #fff; border-radius: 22px; padding: 16px 15px; min-height: 118px; display: flex; flex-direction: column; justify-content: center; box-sizing: border-box; box-shadow: 0 10px 24px rgba(34, 197, 94, .18); } .big-money-card span { font-size: 13px; font-weight: 800; margin-bottom: 8px; opacity: .95; } .big-money-card strong { font-size: 28px; line-height: 1.4; font-weight: 900; } .side-summary { display: flex; flex-direction: column; gap: 10px; } .mini-card { border-radius: 18px; padding: 12px 10px; min-height: 54px; color: #fff; text-align: center; display: flex; flex-direction: column; justify-content: center; } .mini-card span { font-size: 11px; font-weight: 800; margin-bottom: 4px; } .mini-card strong { font-size: 18px; font-weight: 900; } .mini-card.blue { background: linear-gradient(135deg, #2563eb, #3b82f6); } .status-box { background: #fff; border-radius: 18px; padding: 10px; box-shadow: 0 8px 20px rgba(15, 23, 42, .06); } .status-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 2px; color: #334155; font-size: 12px; font-weight: 800; } .status-row strong { color: #0f172a; font-size: 13px; font-weight: 900; } .approved-row { border-top: 1px solid #e2e8f0; margin-top: 2px; padding-top: 10px; } .info-strip.single { margin-bottom: 16px; } .info-item { background: #fff; border-radius: 17px; padding: 11px 12px; box-shadow: 0 7px 18px rgba(15, 23, 42, .05); box-sizing: border-box; } .info-item span { display: block; color: #64748b; font-size: 11px; font-weight: 800; margin-bottom: 5px; } .info-item strong { display: block; color: #0f172a; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .info-item small { color: #94a3b8; font-size: 11px; } .search-highlight { background: linear-gradient(135deg, #dbeafe, #eff6ff); border: 2px solid #93c5fd; border-radius: 22px; padding: 14px; margin-bottom: 18px; box-shadow: 0 10px 24px rgba(37, 99, 235, .12); position: relative; } .search-highlight label { display: block; margin-bottom: 9px; color: #1d4ed8; font-size: 13px; font-weight: 900; } .search-highlight input { width: 100%; height: 56px; border: none; outline: none; border-radius: 16px; background: #fff; padding: 0 15px; box-sizing: border-box; font-size: 15px; font-weight: 700; color: #111827; } .search-results { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; } .result-item { background: #fff; border-radius: 14px; padding: 10px 12px; cursor: pointer; border: 1px solid #dbeafe; } .result-item strong { display: block; font-size: 13px; color: #0f172a; margin-bottom: 4px; } .result-item small { color: #64748b; font-size: 11px; } .result-item:hover { background: #eff6ff; } .section-head { display: flex; justify-content: space-between; align-items: center; margin: 15px 2px 9px; } .section-head span { font-size: 13px; color: #334155; font-weight: 900; } .section-head small { color: #94a3b8; font-size: 11px; } .service-form-card { background: #fff; border-radius: 22px; padding: 15px; box-shadow: 0 10px 28px rgba(15, 23, 42, .07); box-sizing: border-box; } .service-top { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; margin-bottom: 13px; } .service-top h3 { margin: 0 0 6px; font-size: 16px; color: #111827; font-weight: 900; } .service-top p { margin: 0; color: #64748b; font-size: 12px; } .price-tag { background: #eff6ff; color: #2563eb; border-radius: 999px; padding: 8px 10px; font-size: 12px; font-weight: 900; white-space: nowrap; } .form-group { margin-bottom: 11px; } .form-group label { display: block; font-size: 12px; font-weight: 900; color: #475569; margin-bottom: 7px; } .form-group input, .form-group textarea { width: 100%; border: none; outline: none; background: #f8fafc; border-radius: 15px; box-sizing: border-box; font-family: inherit; color: #111827; } .form-group input { height: 48px; padding: 0 13px; font-size: 15px; font-weight: 800; } .form-group textarea { min-height: 78px; resize: none; padding: 12px 13px; font-size: 13px; line-height: 1.8; } .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .submit-btn { width: 100%; height: 52px; border: none; border-radius: 17px; background: #16a34a; color: #fff; font-size: 15px; font-weight: 900; margin-top: 2px; cursor: pointer; } .submit-btn:hover { opacity: .95; } .today-list { display: flex; flex-direction: column; gap: 9px; } .today-item { background: #fff; border-radius: 18px; padding: 13px; box-shadow: 0 7px 18px rgba(15, 23, 42, .05); } .today-item h4 { margin: 0 0 7px; color: #111827; font-size: 14px; font-weight: 900; } .today-item p { margin: 0 0 4px; color: #64748b; font-size: 12px; line-height: 1.7; } .item-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 9px; } .amount { color: #111827; font-size: 13px; font-weight: 900; } .status { border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 900; } .status.pending { background: #fef3c7; color: #92400e; } .status.approved { background: #dcfce7; color: #15803d; } .empty-state { background: #fff; border-radius: 16px; padding: 16px; text-align: center; color: #94a3b8; font-size: 13px; font-weight: 700; } @media (max-width: 390px) { .summary-wrap { grid-template-columns: 1fr; } .big-money-card strong { font-size: 24px; } .section-head small { display: none; } } </style> <script> const services = [ { id: 1, name: 'میز لبه‌دار ۳۵', price: 95000 }, { id: 2, name: 'میز لبه‌دار ۵۰', price: 110000 }, { id: 3, name: 'میز خام', price: 80000 }, { id: 4, name: 'برش MDF', price: 65000 }, { id: 5, name: 'مونتاژ کمد', price: 140000 }, { id: 6, name: 'لبه چسبانی', price: 70000 } ]; let selectedService = null; let records = []; const serviceSearch = document.getElementById('serviceSearch'); const searchResults = document.getElementById('searchResults'); const selectedServiceName = document.getElementById('selectedServiceName'); const selectedServicePriceText = document.getElementById('selectedServicePriceText'); const selectedServicePriceTag = document.getElementById('selectedServicePriceTag'); const countInput = document.getElementById('countInput'); const amountInput = document.getElementById('amountInput'); const descriptionInput = document.getElementById('descriptionInput'); const submitWorkBtn = document.getElementById('submitWorkBtn'); const todayList = document.getElementById('todayList'); const emptyState = document.getElementById('emptyState'); const todayAmount = document.getElementById('todayAmount'); const todayCount = document.getElementById('todayCount'); const pendingCount = document.getElementById('pendingCount'); const approvedAmount = document.getElementById('approvedAmount'); const lastRecordTitle = document.getElementById('lastRecordTitle'); const lastRecordTime = document.getElementById('lastRecordTime'); function formatToman(number) { return Number(number).toLocaleString('fa-IR') + ' تومان'; } function formatNumber(number) { return Number(number).toLocaleString('fa-IR'); } function renderSearchResults(list) { searchResults.innerHTML = ''; if (!list.length) { return; } list.forEach(service => { const item = document.createElement('div'); item.className = 'result-item'; item.innerHTML = ` <strong>${service.name}</strong> <small>قیمت واحد: ${formatToman(service.price)}</small> `; item.addEventListener('click', function () { selectService(service); }); searchResults.appendChild(item); }); } function selectService(service) { selectedService = service; selectedServiceName.textContent = service.name; selectedServicePriceText.textContent = 'قیمت واحد: ' + formatToman(service.price); selectedServicePriceTag.textContent = formatNumber(service.price); serviceSearch.value = service.name; searchResults.innerHTML = ''; countInput.focus(); } serviceSearch.addEventListener('input', function () { const value = this.value.trim(); if (!value) { searchResults.innerHTML = ''; return; } const filtered = services.filter(service => service.name.includes(value) ); renderSearchResults(filtered); }); function getNowText() { const now = new Date(); return now.toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }); } function updateSummary() { const totalAmount = records.reduce((sum, item) => sum + item.total, 0); const totalCount = records.reduce((sum, item) => sum + item.count, 0); const pending = records.filter(item => item.status === 'pending').length; const approved = records .filter(item => item.status === 'approved') .reduce((sum, item) => sum + item.total, 0); todayAmount.textContent = formatToman(totalAmount); todayCount.textContent = formatNumber(totalCount); pendingCount.textContent = formatNumber(pending); approvedAmount.textContent = formatToman(approved); } function renderRecords() { todayList.innerHTML = ''; if (!records.length) { todayList.appendChild(emptyState); return; } records.slice().reverse().forEach(record => { const item = document.createElement('div'); item.className = 'today-item'; item.innerHTML = ` <div class="item-main"> <h4>${record.name}</h4> <p>تعداد: ${formatNumber(record.count)} | مقدار: ${record.amount || '-'}</p> <p>توضیح: ${record.description || '-'}</p> <div class="item-footer"> <span class="amount">${formatToman(record.total)}</span> <span class="status ${record.status}"> ${record.status === 'pending' ? 'در انتظار تایید' : 'تایید شده'} </span> </div> </div> `; item.addEventListener('dblclick', function () { if (record.status === 'pending') { record.status = 'approved'; renderRecords(); updateSummary(); } }); todayList.appendChild(item); }); } submitWorkBtn.addEventListener('click', function () { if (!selectedService) { alert('ابتدا یک خدمت را از جستجو انتخاب کنید.'); return; } const count = parseInt(countInput.value, 10); const amount = amountInput.value.trim(); const description = descriptionInput.value.trim(); if (!count || count < 1) { alert('تعداد معتبر وارد کنید.'); return; } const total = count * selectedService.price; const record = { name: selectedService.name, price: selectedService.price, count: count, amount: amount, description: description, total: total, status: 'pending', time: getNowText() }; records.push(record); lastRecordTitle.textContent = record.name; lastRecordTime.textContent = 'ثبت در ساعت ' + record.time; renderRecords(); updateSummary(); countInput.value = 1; amountInput.value = ''; descriptionInput.value = ''; alert('کارکرد با موفقیت ثبت شد.'); }); updateSummary(); renderRecords(); </script>
<div class="worker-page" dir="rtl">
  <div class="worker-mobile">

    <!-- Header -->
    <div class="worker-header">
      <div>
        <h1>ثبت کارکرد</h1>
        <p>سلام، عرفان</p>
      </div>
      <div class="date-badge">امروز</div>
    </div>

    <!-- Summary -->
    <div class="summary-wrap">
      <div class="big-money-card">
        <span>مبلغ امروز</span>
        <strong id="todayAmount">۰ تومان</strong>
      </div>

      <div class="side-summary">
        <div class="mini-card blue">
          <span>تعداد امروز</span>
          <strong id="todayCount">۰</strong>
        </div>

        <div class="status-box">
          <div class="status-row">
            <span>در انتظار تایید</span>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="status-row approved-row">
            <span>تایید شده</span>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
        </div>
      </div>
    </div>

    <!-- Last Record -->
    <div class="info-strip single">
      <div class="info-item">
        <span>آخرین ثبت</span>
        <strong id="lastRecordTitle">هنوز چیزی ثبت نشده</strong>
        <small id="lastRecordTime">-</small>
      </div>
    </div>

    <!-- Search -->
    <div class="search-highlight">
      <label>جستجوی خدمت</label>
      <input type="text" id="serviceSearch" placeholder="مثلاً: میز لبه‌دار ۳۵" />
      <div class="search-results" id="searchResults"></div>
    </div>

    <!-- Selected Service Form -->
    <div class="section-head">
      <span>خدمت انتخاب‌شده</span>
      <small>اطلاعات کار را وارد کن</small>
    </div>

    <div class="service-form-card">
      <div class="service-top">
        <div>
          <h3 id="selectedServiceName">هنوز خدمتی انتخاب نشده</h3>
          <p id="selectedServicePriceText">قیمت واحد: -</p>
        </div>
        <div class="price-tag" id="selectedServicePriceTag">-</div>
      </div>

      <div class="form-group">
        <label>توضیحات</label>
        <textarea id="descriptionInput" placeholder="مثلاً رنگ، مدل، سفارش خاص، ایراد یا نکته..."></textarea>
      </div>

      <div class="form-row">
        <div class="form-group">
          <label>تعداد</label>
          <input type="number" id="countInput" value="1" min="1" />
        </div>

        <div class="form-group">
          <label>مقدار</label>
          <input type="text" id="amountInput" placeholder="مثلاً ۱۲ متر" />
        </div>
      </div>

      <button type="button" class="submit-btn" id="submitWorkBtn">ثبت کارکرد</button>
    </div>

    <!-- Today Records -->
    <div class="section-head">
      <span>ثبت‌های امروز</span>
      <small>آخرین کارهای ثبت‌شده</small>
    </div>

    <div class="today-list" id="todayList">
      <div class="empty-state" id="emptyState">هنوز کاری ثبت نشده است.</div>
    </div>

  </div>
</div>

<style>
  .worker-page {
    min-height: 100vh;
    background: #e5e7eb;
    display: flex;
    justify-content: center;
    padding: 18px 0;
    box-sizing: border-box;
    font-family: Tahoma, Arial, sans-serif;
  }

  .worker-mobile {
    width: 100%;
    max-width: 430px;
    min-height: 100vh;
    background: #f8fafc;
    border-radius: 28px;
    padding: 16px 14px 30px;
    box-sizing: border-box;
  }

  .worker-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 14px;
  }

  .worker-header h1 {
    margin: 0;
    font-size: 23px;
    font-weight: 900;
    color: #0f172a;
  }

  .worker-header p {
    margin: 6px 0 0;
    font-size: 13px;
    color: #64748b;
  }

  .date-badge {
    background: #111827;
    color: #fff;
    padding: 8px 13px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 800;
  }

  .summary-wrap {
    display: grid;
    grid-template-columns: 1.25fr .95fr;
    gap: 10px;
    margin-bottom: 14px;
  }

  .big-money-card {
    background: linear-gradient(135deg, #16a34a, #22c55e);
    color: #fff;
    border-radius: 22px;
    padding: 16px 15px;
    min-height: 118px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    box-sizing: border-box;
    box-shadow: 0 10px 24px rgba(34, 197, 94, .18);
  }

  .big-money-card span {
    font-size: 13px;
    font-weight: 800;
    margin-bottom: 8px;
    opacity: .95;
  }

  .big-money-card strong {
    font-size: 28px;
    line-height: 1.4;
    font-weight: 900;
  }

  .side-summary {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .mini-card {
    border-radius: 18px;
    padding: 12px 10px;
    min-height: 54px;
    color: #fff;
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .mini-card span {
    font-size: 11px;
    font-weight: 800;
    margin-bottom: 4px;
  }

  .mini-card strong {
    font-size: 18px;
    font-weight: 900;
  }

  .mini-card.blue {
    background: linear-gradient(135deg, #2563eb, #3b82f6);
  }

  .status-box {
    background: #fff;
    border-radius: 18px;
    padding: 10px;
    box-shadow: 0 8px 20px rgba(15, 23, 42, .06);
  }

  .status-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 2px;
    color: #334155;
    font-size: 12px;
    font-weight: 800;
  }

  .status-row strong {
    color: #0f172a;
    font-size: 13px;
    font-weight: 900;
  }

  .approved-row {
    border-top: 1px solid #e2e8f0;
    margin-top: 2px;
    padding-top: 10px;
  }

  .info-strip.single {
    margin-bottom: 16px;
  }

  .info-item {
    background: #fff;
    border-radius: 17px;
    padding: 11px 12px;
    box-shadow: 0 7px 18px rgba(15, 23, 42, .05);
    box-sizing: border-box;
  }

  .info-item span {
    display: block;
    color: #64748b;
    font-size: 11px;
    font-weight: 800;
    margin-bottom: 5px;
  }

  .info-item strong {
    display: block;
    color: #0f172a;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .info-item small {
    color: #94a3b8;
    font-size: 11px;
  }

  .search-highlight {
    background: linear-gradient(135deg, #dbeafe, #eff6ff);
    border: 2px solid #93c5fd;
    border-radius: 22px;
    padding: 14px;
    margin-bottom: 18px;
    box-shadow: 0 10px 24px rgba(37, 99, 235, .12);
    position: relative;
  }

  .search-highlight label {
    display: block;
    margin-bottom: 9px;
    color: #1d4ed8;
    font-size: 13px;
    font-weight: 900;
  }

  .search-highlight input {
    width: 100%;
    height: 56px;
    border: none;
    outline: none;
    border-radius: 16px;
    background: #fff;
    padding: 0 15px;
    box-sizing: border-box;
    font-size: 15px;
    font-weight: 700;
    color: #111827;
  }

  .search-results {
    margin-top: 10px;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .result-item {
    background: #fff;
    border-radius: 14px;
    padding: 10px 12px;
    cursor: pointer;
    border: 1px solid #dbeafe;
  }

  .result-item strong {
    display: block;
    font-size: 13px;
    color: #0f172a;
    margin-bottom: 4px;
  }

  .result-item small {
    color: #64748b;
    font-size: 11px;
  }

  .result-item:hover {
    background: #eff6ff;
  }

  .section-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 15px 2px 9px;
  }

  .section-head span {
    font-size: 13px;
    color: #334155;
    font-weight: 900;
  }

  .section-head small {
    color: #94a3b8;
    font-size: 11px;
  }

  .service-form-card {
    background: #fff;
    border-radius: 22px;
    padding: 15px;
    box-shadow: 0 10px 28px rgba(15, 23, 42, .07);
    box-sizing: border-box;
  }

  .service-top {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    align-items: flex-start;
    margin-bottom: 13px;
  }

  .service-top h3 {
    margin: 0 0 6px;
    font-size: 16px;
    color: #111827;
    font-weight: 900;
  }

  .service-top p {
    margin: 0;
    color: #64748b;
    font-size: 12px;
  }

  .price-tag {
    background: #eff6ff;
    color: #2563eb;
    border-radius: 999px;
    padding: 8px 10px;
    font-size: 12px;
    font-weight: 900;
    white-space: nowrap;
  }

  .form-group {
    margin-bottom: 11px;
  }

  .form-group label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    color: #475569;
    margin-bottom: 7px;
  }

  .form-group input,
  .form-group textarea {
    width: 100%;
    border: none;
    outline: none;
    background: #f8fafc;
    border-radius: 15px;
    box-sizing: border-box;
    font-family: inherit;
    color: #111827;
  }

  .form-group input {
    height: 48px;
    padding: 0 13px;
    font-size: 15px;
    font-weight: 800;
  }

  .form-group textarea {
    min-height: 78px;
    resize: none;
    padding: 12px 13px;
    font-size: 13px;
    line-height: 1.8;
  }

  .form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .submit-btn {
    width: 100%;
    height: 52px;
    border: none;
    border-radius: 17px;
    background: #16a34a;
    color: #fff;
    font-size: 15px;
    font-weight: 900;
    margin-top: 2px;
    cursor: pointer;
  }

  .submit-btn:hover {
    opacity: .95;
  }

  .today-list {
    display: flex;
    flex-direction: column;
    gap: 9px;
  }

  .today-item {
    background: #fff;
    border-radius: 18px;
    padding: 13px;
    box-shadow: 0 7px 18px rgba(15, 23, 42, .05);
  }

  .today-item h4 {
    margin: 0 0 7px;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .today-item p {
    margin: 0 0 4px;
    color: #64748b;
    font-size: 12px;
    line-height: 1.7;
  }

  .item-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 9px;
  }

  .amount {
    color: #111827;
    font-size: 13px;
    font-weight: 900;
  }

  .status {
    border-radius: 999px;
    padding: 5px 10px;
    font-size: 11px;
    font-weight: 900;
  }

  .status.pending {
    background: #fef3c7;
    color: #92400e;
  }

  .status.approved {
    background: #dcfce7;
    color: #15803d;
  }

  .empty-state {
    background: #fff;
    border-radius: 16px;
    padding: 16px;
    text-align: center;
    color: #94a3b8;
    font-size: 13px;
    font-weight: 700;
  }

  @media (max-width: 390px) {
    .summary-wrap {
      grid-template-columns: 1fr;
    }

    .big-money-card strong {
      font-size: 24px;
    }

    .section-head small {
      display: none;
    }
  }
</style>

<script>
  const services = [
    { id: 1, name: 'میز لبه‌دار ۳۵', price: 95000 },
    { id: 2, name: 'میز لبه‌دار ۵۰', price: 110000 },
    { id: 3, name: 'میز خام', price: 80000 },
    { id: 4, name: 'برش MDF', price: 65000 },
    { id: 5, name: 'مونتاژ کمد', price: 140000 },
    { id: 6, name: 'لبه چسبانی', price: 70000 }
  ];

  let selectedService = null;
  let records = [];

  const serviceSearch = document.getElementById('serviceSearch');
  const searchResults = document.getElementById('searchResults');
  const selectedServiceName = document.getElementById('selectedServiceName');
  const selectedServicePriceText = document.getElementById('selectedServicePriceText');
  const selectedServicePriceTag = document.getElementById('selectedServicePriceTag');
  const countInput = document.getElementById('countInput');
  const amountInput = document.getElementById('amountInput');
  const descriptionInput = document.getElementById('descriptionInput');
  const submitWorkBtn = document.getElementById('submitWorkBtn');
  const todayList = document.getElementById('todayList');
  const emptyState = document.getElementById('emptyState');

  const todayAmount = document.getElementById('todayAmount');
  const todayCount = document.getElementById('todayCount');
  const pendingCount = document.getElementById('pendingCount');
  const approvedAmount = document.getElementById('approvedAmount');
  const lastRecordTitle = document.getElementById('lastRecordTitle');
  const lastRecordTime = document.getElementById('lastRecordTime');

  function formatToman(number) {
    return Number(number).toLocaleString('fa-IR') + ' تومان';
  }

  function formatNumber(number) {
    return Number(number).toLocaleString('fa-IR');
  }

  function renderSearchResults(list) {
    searchResults.innerHTML = '';

    if (!list.length) {
      return;
    }

    list.forEach(service => {
      const item = document.createElement('div');
      item.className = 'result-item';
      item.innerHTML = `
        <strong>${service.name}</strong>
        <small>قیمت واحد: ${formatToman(service.price)}</small>
      `;
      item.addEventListener('click', function () {
        selectService(service);
      });
      searchResults.appendChild(item);
    });
  }

  function selectService(service) {
    selectedService = service;
    selectedServiceName.textContent = service.name;
    selectedServicePriceText.textContent = 'قیمت واحد: ' + formatToman(service.price);
    selectedServicePriceTag.textContent = formatNumber(service.price);
    serviceSearch.value = service.name;
    searchResults.innerHTML = '';
    countInput.focus();
  }

  serviceSearch.addEventListener('input', function () {
    const value = this.value.trim();
    if (!value) {
      searchResults.innerHTML = '';
      return;
    }

    const filtered = services.filter(service =>
      service.name.includes(value)
    );

    renderSearchResults(filtered);
  });

  function getNowText() {
    const now = new Date();
    return now.toLocaleTimeString('fa-IR', {
      hour: '2-digit',
      minute: '2-digit'
    });
  }

  function updateSummary() {
    const totalAmount = records.reduce((sum, item) => sum + item.total, 0);
    const totalCount = records.reduce((sum, item) => sum + item.count, 0);
    const pending = records.filter(item => item.status === 'pending').length;
    const approved = records
      .filter(item => item.status === 'approved')
      .reduce((sum, item) => sum + item.total, 0);

    todayAmount.textContent = formatToman(totalAmount);
    todayCount.textContent = formatNumber(totalCount);
    pendingCount.textContent = formatNumber(pending);
    approvedAmount.textContent = formatToman(approved);
  }

  function renderRecords() {
    todayList.innerHTML = '';

    if (!records.length) {
      todayList.appendChild(emptyState);
      return;
    }

    records.slice().reverse().forEach(record => {
      const item = document.createElement('div');
      item.className = 'today-item';

      item.innerHTML = `
        <div class="item-main">
          <h4>${record.name}</h4>
          <p>تعداد: ${formatNumber(record.count)} | مقدار: ${record.amount || '-'}</p>
          <p>توضیح: ${record.description || '-'}</p>
          <div class="item-footer">
            <span class="amount">${formatToman(record.total)}</span>
            <span class="status ${record.status}">
              ${record.status === 'pending' ? 'در انتظار تایید' : 'تایید شده'}
            </span>
          </div>
        </div>
      `;

      item.addEventListener('dblclick', function () {
        if (record.status === 'pending') {
          record.status = 'approved';
          renderRecords();
          updateSummary();
        }
      });

      todayList.appendChild(item);
    });
  }

  submitWorkBtn.addEventListener('click', function () {
    if (!selectedService) {
      alert('ابتدا یک خدمت را از جستجو انتخاب کنید.');
      return;
    }

    const count = parseInt(countInput.value, 10);
    const amount = amountInput.value.trim();
    const description = descriptionInput.value.trim();

    if (!count || count < 1) {
      alert('تعداد معتبر وارد کنید.');
      return;
    }

    const total = count * selectedService.price;

    const record = {
      name: selectedService.name,
      price: selectedService.price,
      count: count,
      amount: amount,
      description: description,
      total: total,
      status: 'pending',
      time: getNowText()
    };

    records.push(record);

    lastRecordTitle.textContent = record.name;
    lastRecordTime.textContent = 'ثبت در ساعت ' + record.time;

    renderRecords();
    updateSummary();

    countInput.value = 1;
    amountInput.value = '';
    descriptionInput.value = '';

    alert('کارکرد با موفقیت ثبت شد.');
  });

  updateSummary();
  renderRecords();
</script>
نمونه اصلی
TEXT - 2026-05-11 22:58:15
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
جددددیدد
TEXT - 2026-05-11 22:58:07
<div class="worker-page" dir="rtl"> <div class="worker-mobile"> <!-- Header --> <div class="worker-header"> <div> <h1>ثبت کارکرد</h1> <p>سلام، عرفان</p> </div> <div class="date-badge">امروز</div> </div> <!-- Compact Summary --> <div class="mini-summary"> <div class="mini-card green"> <span>مبلغ امروز</span> <strong>۳۰۰,۰۰۰</strong> </div> <div class="mini-card blue"> <span>تعداد امروز</span> <strong>۳</strong> </div> <div class="mini-card orange"> <span>در انتظار</span> <strong>۲</strong> </div> <div class="mini-card dark"> <span>تایید شده</span> <strong>۱۱۰,۰۰۰</strong> </div> </div> <!-- Worker Info --> <div class="info-strip single"> <div class="info-item"> <span>آخرین ثبت</span> <strong>میز لبه‌دار ۳۵</strong> <small>۵ دقیقه پیش</small> </div> </div> <!-- Search --> <div class="search-highlight"> <label>جستجوی خدمت</label> <input type="text" placeholder="مثلاً: میز لبه‌دار ۳۵" /> </div> <!-- Selected Service Form --> <div class="section-head"> <span>خدمت انتخاب‌شده</span> <small>اطلاعات کار را وارد کن</small> </div> <div class="service-form-card"> <div class="service-top"> <div> <h3>میز لبه‌دار ۳۵</h3> <p>قیمت واحد: ۹۵,۰۰۰ تومان</p> </div> <div class="price-tag">۹۵,۰۰۰</div> </div> <div class="form-group"> <label>توضیحات</label> <textarea placeholder="مثلاً رنگ، مدل، سفارش خاص، ایراد یا نکته..."></textarea> </div> <div class="form-row"> <div class="form-group"> <label>تعداد</label> <input type="text" value="1" /> </div> <div class="form-group"> <label>مقدار</label> <input type="text" placeholder="مثلاً ۱۲ متر" /> </div> </div> <button type="button" class="submit-btn">ثبت کارکرد</button> </div> <!-- Today Records --> <div class="section-head"> <span>ثبت‌های امروز</span> <small>آخرین کارهای ثبت‌شده</small> </div> <div class="today-list"> <div class="today-item"> <div class="item-main"> <h4>میز لبه‌دار ۳۵</h4> <p>تعداد: ۲ | مقدار: ۱۲ متر</p> <p>توضیح: لبه سفید براق</p> <div class="item-footer"> <span class="amount">۱۹۰,۰۰۰ تومان</span> <span class="status pending">در انتظار تایید</span> </div> </div> </div> <div class="today-item"> <div class="item-main"> <h4>میز لبه‌دار ۵۰</h4> <p>تعداد: ۱ | مقدار: ۶ متر</p> <p>توضیح: بدون رنگ</p> <div class="item-footer"> <span class="amount">۱۱۰,۰۰۰ تومان</span> <span class="status approved">تایید شده</span> </div> </div> </div> </div> </div> </div> <style> .worker-page { min-height: 100vh; background: #e5e7eb; display: flex; justify-content: center; padding: 18px 0; box-sizing: border-box; font-family: Tahoma, Arial, sans-serif; } .worker-mobile { width: 100%; max-width: 430px; min-height: 100vh; background: #f8fafc; border-radius: 28px; padding: 16px 14px 30px; box-sizing: border-box; } .worker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; } .worker-header h1 { margin: 0; font-size: 23px; font-weight: 900; color: #0f172a; } .worker-header p { margin: 6px 0 0; font-size: 13px; color: #64748b; } .date-badge { background: #111827; color: #fff; padding: 8px 13px; border-radius: 999px; font-size: 12px; font-weight: 800; } /* خلاصه کوچک بالا */ .mini-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 12px; } .mini-card { border-radius: 16px; padding: 9px 6px; min-height: 58px; box-sizing: border-box; color: #fff; display: flex; flex-direction: column; justify-content: center; text-align: center; } .mini-card span { font-size: 10px; font-weight: 700; opacity: .95; margin-bottom: 5px; white-space: nowrap; } .mini-card strong { font-size: 13px; font-weight: 900; white-space: nowrap; } .mini-card.green { background: linear-gradient(135deg, #16a34a, #22c55e); } .mini-card.blue { background: linear-gradient(135deg, #2563eb, #3b82f6); } .mini-card.orange { background: linear-gradient(135deg, #f97316, #fb923c); } .mini-card.dark { background: linear-gradient(135deg, #0f172a, #334155); } /* اطلاعات مفید */ .info-strip.single { margin-bottom: 16px; } .info-item { background: #fff; border-radius: 17px; padding: 11px 12px; box-shadow: 0 7px 18px rgba(15, 23, 42, .05); box-sizing: border-box; } .info-item span { display: block; color: #64748b; font-size: 11px; font-weight: 800; margin-bottom: 5px; } .info-item strong { display: block; color: #0f172a; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .info-item small { color: #94a3b8; font-size: 11px; } /* جستجوی برجسته */ .search-highlight { background: linear-gradient(135deg, #dbeafe, #eff6ff); border: 2px solid #93c5fd; border-radius: 22px; padding: 14px; margin-bottom: 18px; box-shadow: 0 10px 24px rgba(37, 99, 235, .12); } .search-highlight label { display: block; margin-bottom: 9px; color: #1d4ed8; font-size: 13px; font-weight: 900; } .search-highlight input { width: 100%; height: 56px; border: none; outline: none; border-radius: 16px; background: #fff; padding: 0 15px; box-sizing: border-box; font-size: 15px; font-weight: 700; color: #111827; } .section-head { display: flex; justify-content: space-between; align-items: center; margin: 15px 2px 9px; } .section-head span { font-size: 13px; color: #334155; font-weight: 900; } .section-head small { color: #94a3b8; font-size: 11px; } .service-form-card { background: #fff; border-radius: 22px; padding: 15px; box-shadow: 0 10px 28px rgba(15, 23, 42, .07); box-sizing: border-box; } .service-top { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; margin-bottom: 13px; } .service-top h3 { margin: 0 0 6px; font-size: 16px; color: #111827; font-weight: 900; } .service-top p { margin: 0; color: #64748b; font-size: 12px; } .price-tag { background: #eff6ff; color: #2563eb; border-radius: 999px; padding: 8px 10px; font-size: 12px; font-weight: 900; white-space: nowrap; } .form-group { margin-bottom: 11px; } .form-group label { display: block; font-size: 12px; font-weight: 900; color: #475569; margin-bottom: 7px; } .form-group input, .form-group textarea { width: 100%; border: none; outline: none; background: #f8fafc; border-radius: 15px; box-sizing: border-box; font-family: inherit; color: #111827; } .form-group input { height: 48px; padding: 0 13px; font-size: 15px; font-weight: 800; } .form-group textarea { min-height: 78px; resize: none; padding: 12px 13px; font-size: 13px; line-height: 1.8; } .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .submit-btn { width: 100%; height: 52px; border: none; border-radius: 17px; background: #16a34a; color: #fff; font-size: 15px; font-weight: 900; margin-top: 2px; } .today-list { display: flex; flex-direction: column; gap: 9px; } .today-item { background: #fff; border-radius: 18px; padding: 13px; box-shadow: 0 7px 18px rgba(15, 23, 42, .05); } .today-item h4 { margin: 0 0 7px; color: #111827; font-size: 14px; font-weight: 900; } .today-item p { margin: 0 0 4px; color: #64748b; font-size: 12px; line-height: 1.7; } .item-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 9px; } .amount { color: #111827; font-size: 13px; font-weight: 900; } .status { border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 900; } .status.pending { background: #fef3c7; color: #92400e; } .status.approved { background: #dcfce7; color: #15803d; } @media (max-width: 380px) { .mini-card span { font-size: 9px; } .mini-card strong { font-size: 11px; } .section-head small { display: none; } } </style>
<div class="worker-page" dir="rtl">
  <div class="worker-mobile">

    <!-- Header -->
    <div class="worker-header">
      <div>
        <h1>ثبت کارکرد</h1>
        <p>سلام، عرفان</p>
      </div>
      <div class="date-badge">امروز</div>
    </div>

    <!-- Compact Summary -->
    <div class="mini-summary">
      <div class="mini-card green">
        <span>مبلغ امروز</span>
        <strong>۳۰۰,۰۰۰</strong>
      </div>

      <div class="mini-card blue">
        <span>تعداد امروز</span>
        <strong>۳</strong>
      </div>

      <div class="mini-card orange">
        <span>در انتظار</span>
        <strong>۲</strong>
      </div>

      <div class="mini-card dark">
        <span>تایید شده</span>
        <strong>۱۱۰,۰۰۰</strong>
      </div>
    </div>

    <!-- Worker Info -->
    <div class="info-strip single">
      <div class="info-item">
        <span>آخرین ثبت</span>
        <strong>میز لبه‌دار ۳۵</strong>
        <small>۵ دقیقه پیش</small>
      </div>
    </div>

    <!-- Search -->
    <div class="search-highlight">
      <label>جستجوی خدمت</label>
      <input type="text" placeholder="مثلاً: میز لبه‌دار ۳۵" />
    </div>

    <!-- Selected Service Form -->
    <div class="section-head">
      <span>خدمت انتخاب‌شده</span>
      <small>اطلاعات کار را وارد کن</small>
    </div>

    <div class="service-form-card">
      <div class="service-top">
        <div>
          <h3>میز لبه‌دار ۳۵</h3>
          <p>قیمت واحد: ۹۵,۰۰۰ تومان</p>
        </div>
        <div class="price-tag">۹۵,۰۰۰</div>
      </div>

      <div class="form-group">
        <label>توضیحات</label>
        <textarea placeholder="مثلاً رنگ، مدل، سفارش خاص، ایراد یا نکته..."></textarea>
      </div>

      <div class="form-row">
        <div class="form-group">
          <label>تعداد</label>
          <input type="text" value="1" />
        </div>

        <div class="form-group">
          <label>مقدار</label>
          <input type="text" placeholder="مثلاً ۱۲ متر" />
        </div>
      </div>

      <button type="button" class="submit-btn">ثبت کارکرد</button>
    </div>

    <!-- Today Records -->
    <div class="section-head">
      <span>ثبت‌های امروز</span>
      <small>آخرین کارهای ثبت‌شده</small>
    </div>

    <div class="today-list">

      <div class="today-item">
        <div class="item-main">
          <h4>میز لبه‌دار ۳۵</h4>
          <p>تعداد: ۲ | مقدار: ۱۲ متر</p>
          <p>توضیح: لبه سفید براق</p>
          <div class="item-footer">
            <span class="amount">۱۹۰,۰۰۰ تومان</span>
            <span class="status pending">در انتظار تایید</span>
          </div>
        </div>
      </div>

      <div class="today-item">
        <div class="item-main">
          <h4>میز لبه‌دار ۵۰</h4>
          <p>تعداد: ۱ | مقدار: ۶ متر</p>
          <p>توضیح: بدون رنگ</p>
          <div class="item-footer">
            <span class="amount">۱۱۰,۰۰۰ تومان</span>
            <span class="status approved">تایید شده</span>
          </div>
        </div>
      </div>

    </div>

  </div>
</div>

<style>
  .worker-page {
    min-height: 100vh;
    background: #e5e7eb;
    display: flex;
    justify-content: center;
    padding: 18px 0;
    box-sizing: border-box;
    font-family: Tahoma, Arial, sans-serif;
  }

  .worker-mobile {
    width: 100%;
    max-width: 430px;
    min-height: 100vh;
    background: #f8fafc;
    border-radius: 28px;
    padding: 16px 14px 30px;
    box-sizing: border-box;
  }

  .worker-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 14px;
  }

  .worker-header h1 {
    margin: 0;
    font-size: 23px;
    font-weight: 900;
    color: #0f172a;
  }

  .worker-header p {
    margin: 6px 0 0;
    font-size: 13px;
    color: #64748b;
  }

  .date-badge {
    background: #111827;
    color: #fff;
    padding: 8px 13px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 800;
  }

  /* خلاصه کوچک بالا */
  .mini-summary {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
    margin-bottom: 12px;
  }

  .mini-card {
    border-radius: 16px;
    padding: 9px 6px;
    min-height: 58px;
    box-sizing: border-box;
    color: #fff;
    display: flex;
    flex-direction: column;
    justify-content: center;
    text-align: center;
  }

  .mini-card span {
    font-size: 10px;
    font-weight: 700;
    opacity: .95;
    margin-bottom: 5px;
    white-space: nowrap;
  }

  .mini-card strong {
    font-size: 13px;
    font-weight: 900;
    white-space: nowrap;
  }

  .mini-card.green {
    background: linear-gradient(135deg, #16a34a, #22c55e);
  }

  .mini-card.blue {
    background: linear-gradient(135deg, #2563eb, #3b82f6);
  }

  .mini-card.orange {
    background: linear-gradient(135deg, #f97316, #fb923c);
  }

  .mini-card.dark {
    background: linear-gradient(135deg, #0f172a, #334155);
  }

  /* اطلاعات مفید */
  .info-strip.single {
    margin-bottom: 16px;
  }

  .info-item {
    background: #fff;
    border-radius: 17px;
    padding: 11px 12px;
    box-shadow: 0 7px 18px rgba(15, 23, 42, .05);
    box-sizing: border-box;
  }

  .info-item span {
    display: block;
    color: #64748b;
    font-size: 11px;
    font-weight: 800;
    margin-bottom: 5px;
  }

  .info-item strong {
    display: block;
    color: #0f172a;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .info-item small {
    color: #94a3b8;
    font-size: 11px;
  }

  /* جستجوی برجسته */
  .search-highlight {
    background: linear-gradient(135deg, #dbeafe, #eff6ff);
    border: 2px solid #93c5fd;
    border-radius: 22px;
    padding: 14px;
    margin-bottom: 18px;
    box-shadow: 0 10px 24px rgba(37, 99, 235, .12);
  }

  .search-highlight label {
    display: block;
    margin-bottom: 9px;
    color: #1d4ed8;
    font-size: 13px;
    font-weight: 900;
  }

  .search-highlight input {
    width: 100%;
    height: 56px;
    border: none;
    outline: none;
    border-radius: 16px;
    background: #fff;
    padding: 0 15px;
    box-sizing: border-box;
    font-size: 15px;
    font-weight: 700;
    color: #111827;
  }

  .section-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 15px 2px 9px;
  }

  .section-head span {
    font-size: 13px;
    color: #334155;
    font-weight: 900;
  }

  .section-head small {
    color: #94a3b8;
    font-size: 11px;
  }

  .service-form-card {
    background: #fff;
    border-radius: 22px;
    padding: 15px;
    box-shadow: 0 10px 28px rgba(15, 23, 42, .07);
    box-sizing: border-box;
  }

  .service-top {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    align-items: flex-start;
    margin-bottom: 13px;
  }

  .service-top h3 {
    margin: 0 0 6px;
    font-size: 16px;
    color: #111827;
    font-weight: 900;
  }

  .service-top p {
    margin: 0;
    color: #64748b;
    font-size: 12px;
  }

  .price-tag {
    background: #eff6ff;
    color: #2563eb;
    border-radius: 999px;
    padding: 8px 10px;
    font-size: 12px;
    font-weight: 900;
    white-space: nowrap;
  }

  .form-group {
    margin-bottom: 11px;
  }

  .form-group label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    color: #475569;
    margin-bottom: 7px;
  }

  .form-group input,
  .form-group textarea {
    width: 100%;
    border: none;
    outline: none;
    background: #f8fafc;
    border-radius: 15px;
    box-sizing: border-box;
    font-family: inherit;
    color: #111827;
  }

  .form-group input {
    height: 48px;
    padding: 0 13px;
    font-size: 15px;
    font-weight: 800;
  }

  .form-group textarea {
    min-height: 78px;
    resize: none;
    padding: 12px 13px;
    font-size: 13px;
    line-height: 1.8;
  }

  .form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .submit-btn {
    width: 100%;
    height: 52px;
    border: none;
    border-radius: 17px;
    background: #16a34a;
    color: #fff;
    font-size: 15px;
    font-weight: 900;
    margin-top: 2px;
  }

  .today-list {
    display: flex;
    flex-direction: column;
    gap: 9px;
  }

  .today-item {
    background: #fff;
    border-radius: 18px;
    padding: 13px;
    box-shadow: 0 7px 18px rgba(15, 23, 42, .05);
  }

  .today-item h4 {
    margin: 0 0 7px;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .today-item p {
    margin: 0 0 4px;
    color: #64748b;
    font-size: 12px;
    line-height: 1.7;
  }

  .item-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 9px;
  }

  .amount {
    color: #111827;
    font-size: 13px;
    font-weight: 900;
  }

  .status {
    border-radius: 999px;
    padding: 5px 10px;
    font-size: 11px;
    font-weight: 900;
  }

  .status.pending {
    background: #fef3c7;
    color: #92400e;
  }

  .status.approved {
    background: #dcfce7;
    color: #15803d;
  }

  @media (max-width: 380px) {
    .mini-card span {
      font-size: 9px;
    }

    .mini-card strong {
      font-size: 11px;
    }

    .section-head small {
      display: none;
    }
  }
</style>
نمونه اصلی
TEXT - 2026-05-11 22:52:25
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
نیپ کارگر
TEXT - 2026-05-11 22:52:19
<div class="worker-page" dir="rtl"> <div class="worker-mobile"> <!-- Header --> <div class="worker-header"> <div> <h1>ثبت کارکرد</h1> <p>سلام، عرفان</p> </div> <div class="date-badge">امروز</div> </div> <!-- Compact Summary --> <div class="mini-summary"> <div class="mini-card green"> <span>مبلغ امروز</span> <strong>۳۰۰,۰۰۰</strong> </div> <div class="mini-card blue"> <span>تعداد امروز</span> <strong>۳</strong> </div> <div class="mini-card orange"> <span>در انتظار</span> <strong>۲</strong> </div> <div class="mini-card dark"> <span>تایید شده</span> <strong>۱۱۰,۰۰۰</strong> </div> </div> <!-- Worker Useful Info --> <div class="info-strip"> <div class="info-item"> <span>آخرین ثبت</span> <strong>میز لبه‌دار ۳۵</strong> <small>۵ دقیقه پیش</small> </div> <div class="info-item"> <span>هدف امروز</span> <strong>۳ از ۱۰</strong> <small>۳۰٪ انجام شده</small> </div> </div> <!-- Search --> <div class="search-box"> <input type="text" placeholder="جستجوی خدمت..." /> </div> <!-- Recent Services --> <div class="section-head"> <span>خدمات اخیر</span> <small>برای ثبت سریع‌تر</small> </div> <div class="recent-services"> <div class="service-chip active">میز لبه‌دار ۳۵</div> <div class="service-chip">میز لبه‌دار ۵۰</div> <div class="service-chip">برش صفحه</div> </div> <!-- Selected Service Form --> <div class="section-head"> <span>خدمت انتخاب‌شده</span> <small>اطلاعات کار را وارد کن</small> </div> <div class="service-form-card"> <div class="service-top"> <div> <h3>میز لبه‌دار ۳۵</h3> <p>قیمت واحد: ۹۵,۰۰۰ تومان</p> </div> <div class="price-tag">۹۵,۰۰۰</div> </div> <div class="form-group"> <label>توضیحات</label> <textarea placeholder="مثلاً رنگ، مدل، سفارش خاص، ایراد یا نکته..."></textarea> </div> <div class="form-row"> <div class="form-group"> <label>تعداد</label> <input type="text" value="1" /> </div> <div class="form-group"> <label>مقدار</label> <input type="text" placeholder="مثلاً ۱۲ متر" /> </div> </div> <button type="button" class="submit-btn">ثبت کارکرد</button> </div> <!-- Today Records --> <div class="section-head"> <span>ثبت‌های امروز</span> <small>آخرین کارهای ثبت‌شده</small> </div> <div class="today-list"> <div class="today-item"> <div class="item-main"> <h4>میز لبه‌دار ۳۵</h4> <p>تعداد: ۲ | مقدار: ۱۲ متر</p> <p>توضیح: لبه سفید براق</p> <div class="item-footer"> <span class="amount">۱۹۰,۰۰۰ تومان</span> <span class="status pending">در انتظار تایید</span> </div> </div> </div> <div class="today-item"> <div class="item-main"> <h4>میز لبه‌دار ۵۰</h4> <p>تعداد: ۱ | مقدار: ۶ متر</p> <p>توضیح: بدون رنگ</p> <div class="item-footer"> <span class="amount">۱۱۰,۰۰۰ تومان</span> <span class="status approved">تایید شده</span> </div> </div> </div> </div> </div> </div> <style> .worker-page { min-height: 100vh; background: #e5e7eb; display: flex; justify-content: center; padding: 18px 0; box-sizing: border-box; font-family: Tahoma, Arial, sans-serif; } .worker-mobile { width: 100%; max-width: 430px; min-height: 100vh; background: #f8fafc; border-radius: 28px; padding: 16px 14px 30px; box-sizing: border-box; } .worker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; } .worker-header h1 { margin: 0; font-size: 23px; font-weight: 900; color: #0f172a; } .worker-header p { margin: 6px 0 0; font-size: 13px; color: #64748b; } .date-badge { background: #111827; color: #fff; padding: 8px 13px; border-radius: 999px; font-size: 12px; font-weight: 800; } /* خلاصه کوچک بالا */ .mini-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 12px; } .mini-card { border-radius: 16px; padding: 9px 6px; min-height: 58px; box-sizing: border-box; color: #fff; display: flex; flex-direction: column; justify-content: center; text-align: center; } .mini-card span { font-size: 10px; font-weight: 700; opacity: .95; margin-bottom: 5px; white-space: nowrap; } .mini-card strong { font-size: 13px; font-weight: 900; white-space: nowrap; } .mini-card.green { background: linear-gradient(135deg, #16a34a, #22c55e); } .mini-card.blue { background: linear-gradient(135deg, #2563eb, #3b82f6); } .mini-card.orange { background: linear-gradient(135deg, #f97316, #fb923c); } .mini-card.dark { background: linear-gradient(135deg, #0f172a, #334155); } /* اطلاعات مفید کارگر */ .info-strip { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-bottom: 14px; } .info-item { background: #fff; border-radius: 17px; padding: 11px 12px; box-shadow: 0 7px 18px rgba(15, 23, 42, .05); box-sizing: border-box; } .info-item span { display: block; color: #64748b; font-size: 11px; font-weight: 800; margin-bottom: 5px; } .info-item strong { display: block; color: #0f172a; font-size: 13px; font-weight: 900; margin-bottom: 4px; } .info-item small { color: #94a3b8; font-size: 11px; } .search-box { margin-bottom: 14px; } .search-box input { width: 100%; height: 52px; border: none; outline: none; border-radius: 18px; background: #fff; box-shadow: 0 8px 22px rgba(15, 23, 42, .06); padding: 0 15px; box-sizing: border-box; font-size: 14px; color: #111827; } .section-head { display: flex; justify-content: space-between; align-items: center; margin: 15px 2px 9px; } .section-head span { font-size: 13px; color: #334155; font-weight: 900; } .section-head small { color: #94a3b8; font-size: 11px; } .recent-services { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; margin-bottom: 10px; } .service-chip { background: #fff; color: #334155; border-radius: 999px; padding: 10px 13px; font-size: 12px; font-weight: 800; white-space: nowrap; box-shadow: 0 6px 16px rgba(15, 23, 42, .05); } .service-chip.active { background: #dcfce7; color: #15803d; } .service-form-card { background: #fff; border-radius: 22px; padding: 15px; box-shadow: 0 10px 28px rgba(15, 23, 42, .07); box-sizing: border-box; } .service-top { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; margin-bottom: 13px; } .service-top h3 { margin: 0 0 6px; font-size: 16px; color: #111827; font-weight: 900; } .service-top p { margin: 0; color: #64748b; font-size: 12px; } .price-tag { background: #eff6ff; color: #2563eb; border-radius: 999px; padding: 8px 10px; font-size: 12px; font-weight: 900; white-space: nowrap; } .form-group { margin-bottom: 11px; } .form-group label { display: block; font-size: 12px; font-weight: 900; color: #475569; margin-bottom: 7px; } .form-group input, .form-group textarea { width: 100%; border: none; outline: none; background: #f8fafc; border-radius: 15px; box-sizing: border-box; font-family: inherit; color: #111827; } .form-group input { height: 48px; padding: 0 13px; font-size: 15px; font-weight: 800; } .form-group textarea { min-height: 78px; resize: none; padding: 12px 13px; font-size: 13px; line-height: 1.8; } .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .submit-btn { width: 100%; height: 52px; border: none; border-radius: 17px; background: #16a34a; color: #fff; font-size: 15px; font-weight: 900; margin-top: 2px; } .today-list { display: flex; flex-direction: column; gap: 9px; } .today-item { background: #fff; border-radius: 18px; padding: 13px; box-shadow: 0 7px 18px rgba(15, 23, 42, .05); } .today-item h4 { margin: 0 0 7px; color: #111827; font-size: 14px; font-weight: 900; } .today-item p { margin: 0 0 4px; color: #64748b; font-size: 12px; line-height: 1.7; } .item-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 9px; } .amount { color: #111827; font-size: 13px; font-weight: 900; } .status { border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 900; } .status.pending { background: #fef3c7; color: #92400e; } .status.approved { background: #dcfce7; color: #15803d; } @media (max-width: 380px) { .mini-card span { font-size: 9px; } .mini-card strong { font-size: 11px; } .section-head small { display: none; } } </style>
<div class="worker-page" dir="rtl">
  <div class="worker-mobile">

    <!-- Header -->
    <div class="worker-header">
      <div>
        <h1>ثبت کارکرد</h1>
        <p>سلام، عرفان</p>
      </div>
      <div class="date-badge">امروز</div>
    </div>

    <!-- Compact Summary -->
    <div class="mini-summary">
      <div class="mini-card green">
        <span>مبلغ امروز</span>
        <strong>۳۰۰,۰۰۰</strong>
      </div>

      <div class="mini-card blue">
        <span>تعداد امروز</span>
        <strong>۳</strong>
      </div>

      <div class="mini-card orange">
        <span>در انتظار</span>
        <strong>۲</strong>
      </div>

      <div class="mini-card dark">
        <span>تایید شده</span>
        <strong>۱۱۰,۰۰۰</strong>
      </div>
    </div>

    <!-- Worker Useful Info -->
    <div class="info-strip">
      <div class="info-item">
        <span>آخرین ثبت</span>
        <strong>میز لبه‌دار ۳۵</strong>
        <small>۵ دقیقه پیش</small>
      </div>

      <div class="info-item">
        <span>هدف امروز</span>
        <strong>۳ از ۱۰</strong>
        <small>۳۰٪ انجام شده</small>
      </div>
    </div>

    <!-- Search -->
    <div class="search-box">
      <input type="text" placeholder="جستجوی خدمت..." />
    </div>

    <!-- Recent Services -->
    <div class="section-head">
      <span>خدمات اخیر</span>
      <small>برای ثبت سریع‌تر</small>
    </div>

    <div class="recent-services">
      <div class="service-chip active">میز لبه‌دار ۳۵</div>
      <div class="service-chip">میز لبه‌دار ۵۰</div>
      <div class="service-chip">برش صفحه</div>
    </div>

    <!-- Selected Service Form -->
    <div class="section-head">
      <span>خدمت انتخاب‌شده</span>
      <small>اطلاعات کار را وارد کن</small>
    </div>

    <div class="service-form-card">
      <div class="service-top">
        <div>
          <h3>میز لبه‌دار ۳۵</h3>
          <p>قیمت واحد: ۹۵,۰۰۰ تومان</p>
        </div>
        <div class="price-tag">۹۵,۰۰۰</div>
      </div>

      <div class="form-group">
        <label>توضیحات</label>
        <textarea placeholder="مثلاً رنگ، مدل، سفارش خاص، ایراد یا نکته..."></textarea>
      </div>

      <div class="form-row">
        <div class="form-group">
          <label>تعداد</label>
          <input type="text" value="1" />
        </div>

        <div class="form-group">
          <label>مقدار</label>
          <input type="text" placeholder="مثلاً ۱۲ متر" />
        </div>
      </div>

      <button type="button" class="submit-btn">ثبت کارکرد</button>
    </div>

    <!-- Today Records -->
    <div class="section-head">
      <span>ثبت‌های امروز</span>
      <small>آخرین کارهای ثبت‌شده</small>
    </div>

    <div class="today-list">

      <div class="today-item">
        <div class="item-main">
          <h4>میز لبه‌دار ۳۵</h4>
          <p>تعداد: ۲ | مقدار: ۱۲ متر</p>
          <p>توضیح: لبه سفید براق</p>
          <div class="item-footer">
            <span class="amount">۱۹۰,۰۰۰ تومان</span>
            <span class="status pending">در انتظار تایید</span>
          </div>
        </div>
      </div>

      <div class="today-item">
        <div class="item-main">
          <h4>میز لبه‌دار ۵۰</h4>
          <p>تعداد: ۱ | مقدار: ۶ متر</p>
          <p>توضیح: بدون رنگ</p>
          <div class="item-footer">
            <span class="amount">۱۱۰,۰۰۰ تومان</span>
            <span class="status approved">تایید شده</span>
          </div>
        </div>
      </div>

    </div>

  </div>
</div>

<style>
  .worker-page {
    min-height: 100vh;
    background: #e5e7eb;
    display: flex;
    justify-content: center;
    padding: 18px 0;
    box-sizing: border-box;
    font-family: Tahoma, Arial, sans-serif;
  }

  .worker-mobile {
    width: 100%;
    max-width: 430px;
    min-height: 100vh;
    background: #f8fafc;
    border-radius: 28px;
    padding: 16px 14px 30px;
    box-sizing: border-box;
  }

  .worker-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 14px;
  }

  .worker-header h1 {
    margin: 0;
    font-size: 23px;
    font-weight: 900;
    color: #0f172a;
  }

  .worker-header p {
    margin: 6px 0 0;
    font-size: 13px;
    color: #64748b;
  }

  .date-badge {
    background: #111827;
    color: #fff;
    padding: 8px 13px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 800;
  }

  /* خلاصه کوچک بالا */
  .mini-summary {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
    margin-bottom: 12px;
  }

  .mini-card {
    border-radius: 16px;
    padding: 9px 6px;
    min-height: 58px;
    box-sizing: border-box;
    color: #fff;
    display: flex;
    flex-direction: column;
    justify-content: center;
    text-align: center;
  }

  .mini-card span {
    font-size: 10px;
    font-weight: 700;
    opacity: .95;
    margin-bottom: 5px;
    white-space: nowrap;
  }

  .mini-card strong {
    font-size: 13px;
    font-weight: 900;
    white-space: nowrap;
  }

  .mini-card.green {
    background: linear-gradient(135deg, #16a34a, #22c55e);
  }

  .mini-card.blue {
    background: linear-gradient(135deg, #2563eb, #3b82f6);
  }

  .mini-card.orange {
    background: linear-gradient(135deg, #f97316, #fb923c);
  }

  .mini-card.dark {
    background: linear-gradient(135deg, #0f172a, #334155);
  }

  /* اطلاعات مفید کارگر */
  .info-strip {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 9px;
    margin-bottom: 14px;
  }

  .info-item {
    background: #fff;
    border-radius: 17px;
    padding: 11px 12px;
    box-shadow: 0 7px 18px rgba(15, 23, 42, .05);
    box-sizing: border-box;
  }

  .info-item span {
    display: block;
    color: #64748b;
    font-size: 11px;
    font-weight: 800;
    margin-bottom: 5px;
  }

  .info-item strong {
    display: block;
    color: #0f172a;
    font-size: 13px;
    font-weight: 900;
    margin-bottom: 4px;
  }

  .info-item small {
    color: #94a3b8;
    font-size: 11px;
  }

  .search-box {
    margin-bottom: 14px;
  }

  .search-box input {
    width: 100%;
    height: 52px;
    border: none;
    outline: none;
    border-radius: 18px;
    background: #fff;
    box-shadow: 0 8px 22px rgba(15, 23, 42, .06);
    padding: 0 15px;
    box-sizing: border-box;
    font-size: 14px;
    color: #111827;
  }

  .section-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 15px 2px 9px;
  }

  .section-head span {
    font-size: 13px;
    color: #334155;
    font-weight: 900;
  }

  .section-head small {
    color: #94a3b8;
    font-size: 11px;
  }

  .recent-services {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    padding-bottom: 4px;
    margin-bottom: 10px;
  }

  .service-chip {
    background: #fff;
    color: #334155;
    border-radius: 999px;
    padding: 10px 13px;
    font-size: 12px;
    font-weight: 800;
    white-space: nowrap;
    box-shadow: 0 6px 16px rgba(15, 23, 42, .05);
  }

  .service-chip.active {
    background: #dcfce7;
    color: #15803d;
  }

  .service-form-card {
    background: #fff;
    border-radius: 22px;
    padding: 15px;
    box-shadow: 0 10px 28px rgba(15, 23, 42, .07);
    box-sizing: border-box;
  }

  .service-top {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    align-items: flex-start;
    margin-bottom: 13px;
  }

  .service-top h3 {
    margin: 0 0 6px;
    font-size: 16px;
    color: #111827;
    font-weight: 900;
  }

  .service-top p {
    margin: 0;
    color: #64748b;
    font-size: 12px;
  }

  .price-tag {
    background: #eff6ff;
    color: #2563eb;
    border-radius: 999px;
    padding: 8px 10px;
    font-size: 12px;
    font-weight: 900;
    white-space: nowrap;
  }

  .form-group {
    margin-bottom: 11px;
  }

  .form-group label {
    display: block;
    font-size: 12px;
    font-weight: 900;
    color: #475569;
    margin-bottom: 7px;
  }

  .form-group input,
  .form-group textarea {
    width: 100%;
    border: none;
    outline: none;
    background: #f8fafc;
    border-radius: 15px;
    box-sizing: border-box;
    font-family: inherit;
    color: #111827;
  }

  .form-group input {
    height: 48px;
    padding: 0 13px;
    font-size: 15px;
    font-weight: 800;
  }

  .form-group textarea {
    min-height: 78px;
    resize: none;
    padding: 12px 13px;
    font-size: 13px;
    line-height: 1.8;
  }

  .form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  .submit-btn {
    width: 100%;
    height: 52px;
    border: none;
    border-radius: 17px;
    background: #16a34a;
    color: #fff;
    font-size: 15px;
    font-weight: 900;
    margin-top: 2px;
  }

  .today-list {
    display: flex;
    flex-direction: column;
    gap: 9px;
  }

  .today-item {
    background: #fff;
    border-radius: 18px;
    padding: 13px;
    box-shadow: 0 7px 18px rgba(15, 23, 42, .05);
  }

  .today-item h4 {
    margin: 0 0 7px;
    color: #111827;
    font-size: 14px;
    font-weight: 900;
  }

  .today-item p {
    margin: 0 0 4px;
    color: #64748b;
    font-size: 12px;
    line-height: 1.7;
  }

  .item-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 9px;
  }

  .amount {
    color: #111827;
    font-size: 13px;
    font-weight: 900;
  }

  .status {
    border-radius: 999px;
    padding: 5px 10px;
    font-size: 11px;
    font-weight: 900;
  }

  .status.pending {
    background: #fef3c7;
    color: #92400e;
  }

  .status.approved {
    background: #dcfce7;
    color: #15803d;
  }

  @media (max-width: 380px) {
    .mini-card span {
      font-size: 9px;
    }

    .mini-card strong {
      font-size: 11px;
    }

    .section-head small {
      display: none;
    }
  }
</style>
نمونه اصلی
TEXT - 2026-05-11 22:45:52
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
ساده تر
TEXT - 2026-05-11 22:45:37
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <div class="app-header"> <div> <h1>ثبت کارکرد</h1> <p>کارگر: عرفان</p> </div> <div class="demo-badge">DEMO</div> </div> <div class="top-summary"> <div class="summary-card green large"> <small>جمع مبلغ امروز</small> <strong>۳۰۰,۰۰۰ تومان</strong> </div> <div class="summary-card dark small"> <small>تعداد کل امروز</small> <strong>۳</strong> </div> </div> <div class="search-box"> <input type="text" placeholder="جستجوی خدمت..." /> </div> <div class="section-title">خدمت انتخاب‌شده</div> <div class="selected-box"> <div class="service-card"> <div class="service-card-top"> <div> <h3>میز لبه‌دار ۳۵</h3> <p>قیمت واحد: ۹۵,۰۰۰ تومان</p> </div> <div class="price-badge">۹۵,۰۰۰ تومان</div> </div> <div class="field-group"> <label>توضیحات</label> <textarea placeholder="مثلاً رنگ، مدل، توضیح سفارش، نکته خاص..."></textarea> </div> <div class="double-fields"> <div class="field-box"> <label>تعداد</label> <input type="text" value="1" /> </div> <div class="field-box"> <label>مقدار</label> <input type="text" placeholder="مثلاً 12 متر" /> </div> </div> <button type="button" class="add-btn">افزودن به لیست امروز</button> </div> </div> <div class="section-title">ثبت‌های امروز</div> <div class="list-box"> <div class="list-row"> <div> <h4>میز لبه‌دار ۳۵</h4> <p> تعداد: ۲ <br /> مقدار: ۱۲ متر <br /> توضیح: لبه سفید براق <br /> جمع: ۱۹۰,۰۰۰ تومان <br /> <span class="status pending">در انتظار تایید</span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> </div> <div class="list-row"> <div> <h4>میز لبه‌دار ۵۰</h4> <p> تعداد: ۱ <br /> مقدار: ۶ متر <br /> توضیح: بدون رنگ <br /> جمع: ۱۱۰,۰۰۰ تومان <br /> <span class="status pending">در انتظار تایید</span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> </div> </div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; padding:20px 0; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; padding:18px 14px 30px; box-sizing:border-box; border-radius:24px; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:24px; color:#0f172a; font-weight:900; } .app-header p{ margin:7px 0 0; color:#64748b; font-size:14px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .top-summary{ display:grid; grid-template-columns:1.5fr .9fr; gap:12px; margin-bottom:16px; align-items:stretch; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card.large{ min-height:120px; display:flex; flex-direction:column; justify-content:center; } .summary-card.small{ min-height:88px; padding:14px; display:flex; flex-direction:column; justify-content:center; } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.92; } .summary-card strong{ font-size:18px; font-weight:900; } .summary-card.small strong{ font-size:22px; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#1db954,#169c45); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:56px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:800; margin:14px 2px 10px; } .selected-box{ margin-top:10px; margin-bottom:12px; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:900; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:900; white-space:nowrap; } .field-group{ margin-bottom:12px; } .field-group label, .field-box label{ display:block; font-size:13px; font-weight:800; color:#475569; margin-bottom:8px; } .field-group textarea{ width:100%; min-height:90px; resize:none; border:none; outline:none; background:#f8fafc; border-radius:16px; padding:14px; box-sizing:border-box; font-family:inherit; font-size:14px; color:#111827; } .double-fields{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .field-box input{ width:100%; height:52px; border:none; outline:none; background:#f8fafc; border-radius:16px; padding:0 14px; box-sizing:border-box; font-size:16px; font-weight:700; color:#111827; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:900; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:900; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.9; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:900; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:900; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } </style>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <div class="app-header">
      <div>
        <h1>ثبت کارکرد</h1>
        <p>کارگر: عرفان</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <div class="top-summary">
      <div class="summary-card green large">
        <small>جمع مبلغ امروز</small>
        <strong>۳۰۰,۰۰۰ تومان</strong>
      </div>

      <div class="summary-card dark small">
        <small>تعداد کل امروز</small>
        <strong>۳</strong>
      </div>
    </div>

    <div class="search-box">
      <input type="text" placeholder="جستجوی خدمت..." />
    </div>

    <div class="section-title">خدمت انتخاب‌شده</div>
    <div class="selected-box">
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>میز لبه‌دار ۳۵</h3>
            <p>قیمت واحد: ۹۵,۰۰۰ تومان</p>
          </div>
          <div class="price-badge">۹۵,۰۰۰ تومان</div>
        </div>

        <div class="field-group">
          <label>توضیحات</label>
          <textarea placeholder="مثلاً رنگ، مدل، توضیح سفارش، نکته خاص..."></textarea>
        </div>

        <div class="double-fields">
          <div class="field-box">
            <label>تعداد</label>
            <input type="text" value="1" />
          </div>
          <div class="field-box">
            <label>مقدار</label>
            <input type="text" placeholder="مثلاً 12 متر" />
          </div>
        </div>

        <button type="button" class="add-btn">افزودن به لیست امروز</button>
      </div>
    </div>

    <div class="section-title">ثبت‌های امروز</div>
    <div class="list-box">
      <div class="list-row">
        <div>
          <h4>میز لبه‌دار ۳۵</h4>
          <p>
            تعداد: ۲
            <br />
            مقدار: ۱۲ متر
            <br />
            توضیح: لبه سفید براق
            <br />
            جمع: ۱۹۰,۰۰۰ تومان
            <br />
            <span class="status pending">در انتظار تایید</span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      </div>

      <div class="list-row">
        <div>
          <h4>میز لبه‌دار ۵۰</h4>
          <p>
            تعداد: ۱
            <br />
            مقدار: ۶ متر
            <br />
            توضیح: بدون رنگ
            <br />
            جمع: ۱۱۰,۰۰۰ تومان
            <br />
            <span class="status pending">در انتظار تایید</span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      </div>
    </div>

  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
    padding:20px 0;
  }

  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    padding:18px 14px 30px;
    box-sizing:border-box;
    border-radius:24px;
  }

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

  .app-header h1{
    margin:0;
    font-size:24px;
    color:#0f172a;
    font-weight:900;
  }

  .app-header p{
    margin:7px 0 0;
    color:#64748b;
    font-size:14px;
  }

  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }

  .top-summary{
    display:grid;
    grid-template-columns:1.5fr .9fr;
    gap:12px;
    margin-bottom:16px;
    align-items:stretch;
  }

  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }

  .summary-card.large{
    min-height:120px;
    display:flex;
    flex-direction:column;
    justify-content:center;
  }

  .summary-card.small{
    min-height:88px;
    padding:14px;
    display:flex;
    flex-direction:column;
    justify-content:center;
  }

  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.92;
  }

  .summary-card strong{
    font-size:18px;
    font-weight:900;
  }

  .summary-card.small strong{
    font-size:22px;
  }

  .summary-card.dark{
    background:linear-gradient(135deg,#0f172a,#1e293b);
  }

  .summary-card.green{
    background:linear-gradient(135deg,#1db954,#169c45);
  }

  .search-box{
    margin-bottom:14px;
  }

  .search-box input{
    width:100%;
    height:56px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:800;
    margin:14px 2px 10px;
  }

  .selected-box{
    margin-top:10px;
    margin-bottom:12px;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }

  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }

  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:900;
  }

  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }

  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:900;
    white-space:nowrap;
  }

  .field-group{
    margin-bottom:12px;
  }

  .field-group label,
  .field-box label{
    display:block;
    font-size:13px;
    font-weight:800;
    color:#475569;
    margin-bottom:8px;
  }

  .field-group textarea{
    width:100%;
    min-height:90px;
    resize:none;
    border:none;
    outline:none;
    background:#f8fafc;
    border-radius:16px;
    padding:14px;
    box-sizing:border-box;
    font-family:inherit;
    font-size:14px;
    color:#111827;
  }

  .double-fields{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }

  .field-box input{
    width:100%;
    height:52px;
    border:none;
    outline:none;
    background:#f8fafc;
    border-radius:16px;
    padding:0 14px;
    box-sizing:border-box;
    font-size:16px;
    font-weight:700;
    color:#111827;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:900;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }

  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:900;
  }

  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.9;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }

  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:900;
    min-width:72px;
  }

  .btn-remove{
    background:#fee2e2;
    color:#dc2626;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:900;
    margin-top:6px;
  }

  .status.pending{
    background:#fef3c7;
    color:#92400e;
  }
</style>
نمونه اصلی
TEXT - 2026-05-11 22:34:21
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
فقط اچ تی ام ال
TEXT - 2026-05-11 22:34:13
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <div class="app-header"> <div> <h1>ثبت کارکرد</h1> <p>کارگر: عرفان</p> </div> <div class="demo-badge">DEMO</div> </div> <div class="summary-grid"> <div class="summary-card dark"> <small>تعداد کل امروز</small> <strong>۳</strong> </div> <div class="summary-card green"> <small>جمع مبلغ امروز</small> <strong>۳۰۰,۰۰۰ تومان</strong> </div> </div> <div class="search-box"> <input type="text" placeholder="جستجوی سریع خدمت..." /> </div> <div class="section-title">خدمات آماده ثبت</div> <div class="chips"> <div class="chip active">میز لبه‌دار ۳۵</div> <div class="chip">میز لبه‌دار ۴۲</div> <div class="chip">میز لبه‌دار ۵۰</div> <div class="chip">میز لبه‌دار ۶۰</div> <div class="chip">میز لبه‌دار ۷۰</div> </div> <div class="selected-box"> <div class="service-card"> <div class="service-card-top"> <div> <h3>میز لبه‌دار ۳۵</h3> <p>قیمت واحد: ۹۵,۰۰۰ تومان</p> </div> <div class="price-badge">۹۵,۰۰۰ تومان</div> </div> <div class="counter"> <button type="button">−</button> <input type="text" value="1" /> <button type="button">+</button> </div> <button type="button" class="add-btn">افزودن به لیست امروز</button> </div> </div> <div class="section-title">ثبت‌های امروز</div> <div class="list-box"> <div class="list-row"> <div> <h4>میز لبه‌دار ۳۵</h4> <p> ۲ عدد × ۹۵,۰۰۰ تومان <br /> جمع: ۱۹۰,۰۰۰ تومان <br /> <span class="status pending">در انتظار تایید</span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> </div> <div class="list-row"> <div> <h4>میز لبه‌دار ۵۰</h4> <p> ۱ عدد × ۱۱۰,۰۰۰ تومان <br /> جمع: ۱۱۰,۰۰۰ تومان <br /> <span class="status pending">در انتظار تایید</span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> </div> </div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; padding:20px 0; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 30px; box-sizing:border-box; border-radius:24px; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:24px; color:#0f172a; font-weight:900; } .app-header p{ margin:7px 0 0; color:#64748b; font-size:14px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:900; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:800; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:8px; } .chip{ background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:800; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:12px; min-height:110px; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:900; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:900; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:900; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:900; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:900; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:900; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:900; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:900; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } </style>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <div class="app-header">
      <div>
        <h1>ثبت کارکرد</h1>
        <p>کارگر: عرفان</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <div class="summary-grid">
      <div class="summary-card dark">
        <small>تعداد کل امروز</small>
        <strong>۳</strong>
      </div>
      <div class="summary-card green">
        <small>جمع مبلغ امروز</small>
        <strong>۳۰۰,۰۰۰ تومان</strong>
      </div>
    </div>

    <div class="search-box">
      <input type="text" placeholder="جستجوی سریع خدمت..." />
    </div>

    <div class="section-title">خدمات آماده ثبت</div>
    <div class="chips">
      <div class="chip active">میز لبه‌دار ۳۵</div>
      <div class="chip">میز لبه‌دار ۴۲</div>
      <div class="chip">میز لبه‌دار ۵۰</div>
      <div class="chip">میز لبه‌دار ۶۰</div>
      <div class="chip">میز لبه‌دار ۷۰</div>
    </div>

    <div class="selected-box">
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>میز لبه‌دار ۳۵</h3>
            <p>قیمت واحد: ۹۵,۰۰۰ تومان</p>
          </div>
          <div class="price-badge">۹۵,۰۰۰ تومان</div>
        </div>

        <div class="counter">
          <button type="button">−</button>
          <input type="text" value="1" />
          <button type="button">+</button>
        </div>

        <button type="button" class="add-btn">افزودن به لیست امروز</button>
      </div>
    </div>

    <div class="section-title">ثبت‌های امروز</div>
    <div class="list-box">
      <div class="list-row">
        <div>
          <h4>میز لبه‌دار ۳۵</h4>
          <p>
            ۲ عدد × ۹۵,۰۰۰ تومان
            <br />
            جمع: ۱۹۰,۰۰۰ تومان
            <br />
            <span class="status pending">در انتظار تایید</span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      </div>

      <div class="list-row">
        <div>
          <h4>میز لبه‌دار ۵۰</h4>
          <p>
            ۱ عدد × ۱۱۰,۰۰۰ تومان
            <br />
            جمع: ۱۱۰,۰۰۰ تومان
            <br />
            <span class="status pending">در انتظار تایید</span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      </div>
    </div>

  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
    padding:20px 0;
  }

  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 30px;
    box-sizing:border-box;
    border-radius:24px;
  }

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

  .app-header h1{
    margin:0;
    font-size:24px;
    color:#0f172a;
    font-weight:900;
  }

  .app-header p{
    margin:7px 0 0;
    color:#64748b;
    font-size:14px;
  }

  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }

  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }

  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }

  .summary-card strong{
    font-size:18px;
    font-weight:900;
  }

  .summary-card.dark{
    background:linear-gradient(135deg,#0f172a,#1e293b);
  }

  .summary-card.green{
    background:linear-gradient(135deg,#16a34a,#15803d);
  }

  .search-box{
    margin-bottom:14px;
  }

  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:800;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:8px;
  }

  .chip{
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:800;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
  }

  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:12px;
    min-height:110px;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }

  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }

  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:900;
  }

  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }

  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:900;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }

  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:900;
  }

  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:900;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:900;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }

  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:900;
  }

  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }

  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:900;
    min-width:72px;
  }

  .btn-remove{
    background:#fee2e2;
    color:#dc2626;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:900;
    margin-top:6px;
  }

  .status.pending{
    background:#fef3c7;
    color:#92400e;
  }
</style>
نمونه اصلی
TEXT - 2026-05-11 22:24:26
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
صفحه اصلی
TEXT - 2026-05-11 22:24:22
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>ثبت کارکرد</h1> <p>کارگر: عرفان</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Summary --> <div class="summary-grid"> <div class="summary-card dark"> <small>تعداد کل امروز</small> <strong id="regTotalQty">۰</strong> </div> <div class="summary-card green"> <small>جمع مبلغ امروز</small> <strong id="regTotalPrice">۰ تومان</strong> </div> </div> <!-- Search --> <div class="search-box"> <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..."> </div> <!-- Services --> <div class="section-title">خدمات آماده ثبت</div> <div class="chips" id="serviceChips"></div> <!-- Selected Service --> <div id="selectedServiceBox" class="selected-box"> <div class="empty-box">یک خدمت را انتخاب کن</div> </div> <!-- Today List --> <div class="section-title">ثبت‌های امروز</div> <div class="list-box" id="todayItems"> <div class="empty-list">هنوز چیزی ثبت نشده</div> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 30px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:24px; color:#0f172a; font-weight:900; } .app-header p{ margin:7px 0 0; color:#64748b; font-size:14px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:900; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:800; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:8px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:800; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:12px; min-height:110px; } .empty-box, .empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:900; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:900; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:900; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:900; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:900; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:900; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:900; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:900; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .toast{ position:fixed; bottom:24px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } </style> <script> (function(){ const services = [ { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 }, { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 }, { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 }, { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 }, { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 }, { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 } ]; let selectedService = null; let currentQty = 1; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-05" } ]; const todayStr = "2026-05-05"; const chipsBox = document.getElementById("serviceChips"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const todayItems = document.getElementById("todayItems"); const serviceSearch = document.getElementById("serviceSearch"); const toast = document.getElementById("toast"); function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => { toast.classList.remove("show"); }, 1600); } function getAmount(item){ return item.qty * item.price; } function getFilteredServices(){ const q = normalizeText(serviceSearch.value.trim()); if(!q) return services; return services.filter(service => normalizeText(service.name).includes(q)); } function renderChips(list = services){ chipsBox.innerHTML = ""; if(list.length === 0){ chipsBox.innerHTML = `<div class="empty-list" style="min-width:100%;">خدمتی پیدا نشد</div>`; return; } list.forEach(service => { const btn = document.createElement("button"); btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : ""); btn.textContent = service.name; btn.onclick = function(){ selectedService = service; currentQty = 1; renderChips(getFilteredServices()); renderSelectedService(); }; chipsBox.appendChild(btn); }); } function renderSelectedService(){ if(!selectedService){ selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`; return; } selectedServiceBox.innerHTML = ` <div class="service-card"> <div class="service-card-top"> <div> <h3>${selectedService.name}</h3> <p>قیمت واحد: ${money(selectedService.price)}</p> </div> <div class="price-badge">${money(selectedService.price * currentQty)}</div> </div> <div class="counter"> <button type="button" id="minusQty">−</button> <input type="number" id="qtyInput" min="1" value="${currentQty}"> <button type="button" id="plusQty">+</button> </div> <button type="button" class="add-btn" id="addTodayBtn"> افزودن به لیست امروز </button> </div> `; document.getElementById("minusQty").onclick = function(){ currentQty = Math.max(1, currentQty - 1); renderSelectedService(); }; document.getElementById("plusQty").onclick = function(){ currentQty++; renderSelectedService(); }; document.getElementById("qtyInput").oninput = function(e){ currentQty = Math.max(1, parseInt(e.target.value || "1")); renderSelectedService(); }; document.getElementById("addTodayBtn").onclick = function(){ entries.unshift({ id: Date.now(), serviceId: selectedService.id, name: selectedService.name, price: selectedService.price, qty: currentQty, status: "pending", worker: "عرفان", date: todayStr }); currentQty = 1; renderAll(); showToast("ثبت جدید اضافه شد"); }; } function renderTodayItems(){ const todayEntries = entries.filter(item => item.date === todayStr); if(todayEntries.length === 0){ todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`; return; } todayItems.innerHTML = ""; todayEntries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> ${toFa(item.qty)} عدد × ${money(item.price)} <br> جمع: ${money(getAmount(item))} <br> <span class="status pending">در انتظار تایید</span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> `; row.querySelector(".btn-remove").onclick = function(){ entries = entries.filter(e => e.id !== item.id); renderAll(); showToast("آیتم حذف شد"); }; todayItems.appendChild(row); }); } function renderRegisterSummary(){ const todayEntries = entries.filter(item => item.date === todayStr); const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0); const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("regTotalQty").textContent = toFa(totalQty); document.getElementById("regTotalPrice").textContent = money(totalPrice); } function renderAll(){ renderChips(getFilteredServices()); renderSelectedService(); renderTodayItems(); renderRegisterSummary(); } serviceSearch.addEventListener("input", function(){ renderChips(getFilteredServices()); }); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>ثبت کارکرد</h1>
        <p>کارگر: عرفان</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Summary -->
    <div class="summary-grid">
      <div class="summary-card dark">
        <small>تعداد کل امروز</small>
        <strong id="regTotalQty">۰</strong>
      </div>
      <div class="summary-card green">
        <small>جمع مبلغ امروز</small>
        <strong id="regTotalPrice">۰ تومان</strong>
      </div>
    </div>

    <!-- Search -->
    <div class="search-box">
      <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت...">
    </div>

    <!-- Services -->
    <div class="section-title">خدمات آماده ثبت</div>
    <div class="chips" id="serviceChips"></div>

    <!-- Selected Service -->
    <div id="selectedServiceBox" class="selected-box">
      <div class="empty-box">یک خدمت را انتخاب کن</div>
    </div>

    <!-- Today List -->
    <div class="section-title">ثبت‌های امروز</div>
    <div class="list-box" id="todayItems">
      <div class="empty-list">هنوز چیزی ثبت نشده</div>
    </div>

    <div class="toast" id="toast">انجام شد</div>

  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }

  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 30px;
    box-sizing:border-box;
  }

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

  .app-header h1{
    margin:0;
    font-size:24px;
    color:#0f172a;
    font-weight:900;
  }

  .app-header p{
    margin:7px 0 0;
    color:#64748b;
    font-size:14px;
  }

  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }

  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }

  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }

  .summary-card strong{
    font-size:18px;
    font-weight:900;
  }

  .summary-card.dark{
    background:linear-gradient(135deg,#0f172a,#1e293b);
  }

  .summary-card.green{
    background:linear-gradient(135deg,#16a34a,#15803d);
  }

  .search-box{
    margin-bottom:14px;
  }

  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:800;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:8px;
    scrollbar-width:none;
  }

  .chips::-webkit-scrollbar{
    display:none;
  }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:800;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }

  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:12px;
    min-height:110px;
  }

  .empty-box,
  .empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }

  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }

  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:900;
  }

  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }

  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:900;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }

  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:900;
    cursor:pointer;
  }

  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:900;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:900;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }

  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:900;
  }

  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }

  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:900;
    cursor:pointer;
    min-width:72px;
  }

  .btn-remove{
    background:#fee2e2;
    color:#dc2626;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:900;
    margin-top:6px;
  }

  .status.pending{
    background:#fef3c7;
    color:#92400e;
  }

  .toast{
    position:fixed;
    bottom:24px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }

  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }
</style>

<script>
(function(){
  const services = [
    { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 },
    { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 },
    { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 },
    { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 },
    { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 },
    { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 }
  ];

  let selectedService = null;
  let currentQty = 1;

  let entries = [
    {
      id: 1001,
      serviceId: 1,
      name: "میز لبه‌دار ۳۵",
      price: 95000,
      qty: 2,
      status: "pending",
      worker: "عرفان",
      date: "2026-05-05"
    },
    {
      id: 1002,
      serviceId: 3,
      name: "میز لبه‌دار ۵۰",
      price: 110000,
      qty: 1,
      status: "pending",
      worker: "عرفان",
      date: "2026-05-05"
    }
  ];

  const todayStr = "2026-05-05";

  const chipsBox = document.getElementById("serviceChips");
  const selectedServiceBox = document.getElementById("selectedServiceBox");
  const todayItems = document.getElementById("todayItems");
  const serviceSearch = document.getElementById("serviceSearch");
  const toast = document.getElementById("toast");

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => {
      toast.classList.remove("show");
    }, 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function getFilteredServices(){
    const q = normalizeText(serviceSearch.value.trim());
    if(!q) return services;
    return services.filter(service => normalizeText(service.name).includes(q));
  }

  function renderChips(list = services){
    chipsBox.innerHTML = "";

    if(list.length === 0){
      chipsBox.innerHTML = `<div class="empty-list" style="min-width:100%;">خدمتی پیدا نشد</div>`;
      return;
    }

    list.forEach(service => {
      const btn = document.createElement("button");
      btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : "");
      btn.textContent = service.name;

      btn.onclick = function(){
        selectedService = service;
        currentQty = 1;
        renderChips(getFilteredServices());
        renderSelectedService();
      };

      chipsBox.appendChild(btn);
    });
  }

  function renderSelectedService(){
    if(!selectedService){
      selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`;
      return;
    }

    selectedServiceBox.innerHTML = `
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>${selectedService.name}</h3>
            <p>قیمت واحد: ${money(selectedService.price)}</p>
          </div>
          <div class="price-badge">${money(selectedService.price * currentQty)}</div>
        </div>

        <div class="counter">
          <button type="button" id="minusQty">−</button>
          <input type="number" id="qtyInput" min="1" value="${currentQty}">
          <button type="button" id="plusQty">+</button>
        </div>

        <button type="button" class="add-btn" id="addTodayBtn">
          افزودن به لیست امروز
        </button>
      </div>
    `;

    document.getElementById("minusQty").onclick = function(){
      currentQty = Math.max(1, currentQty - 1);
      renderSelectedService();
    };

    document.getElementById("plusQty").onclick = function(){
      currentQty++;
      renderSelectedService();
    };

    document.getElementById("qtyInput").oninput = function(e){
      currentQty = Math.max(1, parseInt(e.target.value || "1"));
      renderSelectedService();
    };

    document.getElementById("addTodayBtn").onclick = function(){
      entries.unshift({
        id: Date.now(),
        serviceId: selectedService.id,
        name: selectedService.name,
        price: selectedService.price,
        qty: currentQty,
        status: "pending",
        worker: "عرفان",
        date: todayStr
      });

      currentQty = 1;
      renderAll();
      showToast("ثبت جدید اضافه شد");
    };
  }

  function renderTodayItems(){
    const todayEntries = entries.filter(item => item.date === todayStr);

    if(todayEntries.length === 0){
      todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`;
      return;
    }

    todayItems.innerHTML = "";

    todayEntries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";

      row.innerHTML = `
        <div>
          <h4>${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد × ${money(item.price)}
            <br>
            جمع: ${money(getAmount(item))}
            <br>
            <span class="status pending">در انتظار تایید</span>
          </p>
        </div>

        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      `;

      row.querySelector(".btn-remove").onclick = function(){
        entries = entries.filter(e => e.id !== item.id);
        renderAll();
        showToast("آیتم حذف شد");
      };

      todayItems.appendChild(row);
    });
  }

  function renderRegisterSummary(){
    const todayEntries = entries.filter(item => item.date === todayStr);

    const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0);
    const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("regTotalQty").textContent = toFa(totalQty);
    document.getElementById("regTotalPrice").textContent = money(totalPrice);
  }

  function renderAll(){
    renderChips(getFilteredServices());
    renderSelectedService();
    renderTodayItems();
    renderRegisterSummary();
  }

  serviceSearch.addEventListener("input", function(){
    renderChips(getFilteredServices());
  });

  renderAll();
})();
</script>
نمونه اصلی
TEXT - 2026-05-11 22:24:05
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
نمونه اصلی
TEXT - 2026-05-11 22:14:21
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
مستقل
TEXT - 2026-05-11 22:14:12
<!doctype html> <html lang="fa" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ثبت کارکرد</title> <style> body { margin: 0; font-family: Arial, sans-serif; background: #f3f5f7; direction: rtl; } .factory-app { min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 20px; box-sizing: border-box; } .factory-phone { width: 100%; max-width: 420px; background: #fff; border-radius: 24px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.08); border: 1px solid #e8e8e8; } .app-header { display: flex; justify-content: space-between; align-items: center; padding: 18px 16px; background: #0f172a; color: #fff; } .app-header h1 { margin: 0; font-size: 20px; line-height: 1.4; } .app-header p { margin: 4px 0 0; font-size: 13px; opacity: 0.8; } .demo-badge { background: #22c55e; color: #fff; font-size: 12px; font-weight: bold; padding: 6px 10px; border-radius: 999px; } .pages-wrap { padding: 16px; background: #f8fafc; } .page { display: block; background: #fff; border-radius: 18px; padding: 16px; box-sizing: border-box; } .page-title h2 { margin: 0; font-size: 18px; color: #111827; } .page-title span { display: inline-block; margin-top: 6px; font-size: 13px; color: #6b7280; } </style> </head> <body> <div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span> </div> </div> </section> </div> </div> </div> </body> </html>
<!doctype html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>ثبت کارکرد</title>
  <style>
    body {
      margin: 0;
      font-family: Arial, sans-serif;
      background: #f3f5f7;
      direction: rtl;
    }

    .factory-app {
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: flex-start;
      padding: 20px;
      box-sizing: border-box;
    }

    .factory-phone {
      width: 100%;
      max-width: 420px;
      background: #fff;
      border-radius: 24px;
      overflow: hidden;
      box-shadow: 0 10px 30px rgba(0,0,0,0.08);
      border: 1px solid #e8e8e8;
    }

    .app-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 18px 16px;
      background: #0f172a;
      color: #fff;
    }

    .app-header h1 {
      margin: 0;
      font-size: 20px;
      line-height: 1.4;
    }

    .app-header p {
      margin: 4px 0 0;
      font-size: 13px;
      opacity: 0.8;
    }

    .demo-badge {
      background: #22c55e;
      color: #fff;
      font-size: 12px;
      font-weight: bold;
      padding: 6px 10px;
      border-radius: 999px;
    }

    .pages-wrap {
      padding: 16px;
      background: #f8fafc;
    }

    .page {
      display: block;
      background: #fff;
      border-radius: 18px;
      padding: 16px;
      box-sizing: border-box;
    }

    .page-title h2 {
      margin: 0;
      font-size: 18px;
      color: #111827;
    }

    .page-title span {
      display: inline-block;
      margin-top: 6px;
      font-size: 13px;
      color: #6b7280;
    }
  </style>
</head>
<body>
  <div class="factory-app" dir="rtl">
    <div class="factory-phone">

      <!-- Header -->
      <div class="app-header">
        <div>
          <h1>سامانه ثبت کارکرد</h1>
          <p>نسخه نمایشی موبایلی</p>
        </div>
        <div class="demo-badge">DEMO</div>
      </div>

      <!-- Pages -->
      <div class="pages-wrap">

        <!-- ثبت کارکرد -->
        <section class="page active" id="page-register">
          <div class="page-title">
            <div>
              <h2>ثبت کارکرد</h2>
              <span>کارگر: عرفان</span>
            </div>
          </div>
        </section>

      </div>
    </div>
  </div>
</body>
</html>
نمونه اصلی
TEXT - 2026-05-11 22:10:47
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
کد نمونه
TEXT - 2026-05-11 21:26:58
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span> </div> </div> <div class="summary-grid"> <div class="summary-card dark"> <small>تعداد کل امروز</small> <strong id="regTotalQty">۰</strong> </div> <div class="summary-card green"> <small>جمع مبلغ امروز</small> <strong id="regTotalPrice">۰ تومان</strong> </div> </div> <div class="search-box"> <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..."> </div> <div class="section-title">خدمات پرکاربرد</div> <div class="chips" id="serviceChips"></div> <div id="selectedServiceBox" class="selected-box"> <div class="empty-box">یک خدمت را انتخاب کن</div> </div> <div class="section-title">ثبت‌های امروز</div> <div class="list-box" id="todayItems"> <div class="empty-list">هنوز چیزی ثبت نشده</div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 95px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; grid-template-columns:repeat(4,1fr); gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:12px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:86px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } @media (max-width:360px){ .factory-phone{ padding:16px 12px 95px; } .mini-grid.three{ grid-template-columns:1fr; } .manager-grid{ grid-template-columns:1fr 1fr; } } </style> <script> (function(){ const services = [ { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 }, { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 }, { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 }, { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 }, { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 }, { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 } ]; let selectedService = null; let currentQty = 1; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان" } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const chipsBox = document.getElementById("serviceChips"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const todayItems = document.getElementById("todayItems"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const serviceSearch = document.getElementById("serviceSearch"); const toast = document.getElementById("toast"); function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getFilteredServices(){ const q = normalizeText(serviceSearch.value.trim()); if(!q) return services; return services.filter(s => normalizeText(s.name).includes(q)); } function renderChips(list = services){ chipsBox.innerHTML = ""; list.forEach(service => { const btn = document.createElement("button"); btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : ""); btn.textContent = service.name; btn.onclick = function(){ selectedService = service; currentQty = 1; renderChips(getFilteredServices()); renderSelectedService(); }; chipsBox.appendChild(btn); }); } function renderSelectedService(){ if(!selectedService){ selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`; return; } selectedServiceBox.innerHTML = ` <div class="service-card"> <div class="service-card-top"> <div> <h3>${selectedService.name}</h3> <p>قیمت واحد: ${money(selectedService.price)}</p> </div> <div class="price-badge">${money(selectedService.price * currentQty)}</div> </div> <div class="counter"> <button type="button" id="minusQty">−</button> <input type="number" id="qtyInput" min="1" value="${currentQty}"> <button type="button" id="plusQty">+</button> </div> <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button> </div> `; document.getElementById("minusQty").onclick = function(){ currentQty = Math.max(1, currentQty - 1); renderSelectedService(); }; document.getElementById("plusQty").onclick = function(){ currentQty++; renderSelectedService(); }; document.getElementById("qtyInput").oninput = function(e){ currentQty = Math.max(1, parseInt(e.target.value || "1")); renderSelectedService(); }; document.getElementById("addTodayBtn").onclick = function(){ entries.unshift({ id: Date.now(), serviceId: selectedService.id, name: selectedService.name, price: selectedService.price, qty: currentQty, status: "pending", worker: "عرفان" }); currentQty = 1; renderAll(); showToast("ثبت جدید اضافه شد"); }; } function renderTodayItems(){ if(entries.length === 0){ todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`; return; } todayItems.innerHTML = ""; entries.forEach((item, index) => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(item.qty * item.price)} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> `; row.querySelector(".btn-remove").onclick = function(){ entries.splice(index, 1); renderAll(); showToast("آیتم حذف شد"); }; todayItems.appendChild(row); }); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(item.qty * item.price)} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + (item.qty * item.price), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + (item.qty * item.price), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + (item.qty * item.price), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> ${toFa(item.qty)} عدد | ${money(item.qty * item.price)} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + item.qty * item.price, 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += item.qty * item.price; }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderRegisterSummary(){ const totalQty = entries.reduce((sum, item) => sum + item.qty, 0); const totalPrice = entries.reduce((sum, item) => sum + item.qty * item.price, 0); document.getElementById("regTotalQty").textContent = toFa(totalQty); document.getElementById("regTotalPrice").textContent = money(totalPrice); } function renderAll(){ renderChips(getFilteredServices()); renderSelectedService(); renderTodayItems(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderRegisterSummary(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function(){ renderChips(getFilteredServices()); }); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
          </div>
        </div>

        <div class="summary-grid">
          <div class="summary-card dark">
            <small>تعداد کل امروز</small>
            <strong id="regTotalQty">۰</strong>
          </div>
          <div class="summary-card green">
            <small>جمع مبلغ امروز</small>
            <strong id="regTotalPrice">۰ تومان</strong>
          </div>
        </div>

        <div class="search-box">
          <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت...">
        </div>

        <div class="section-title">خدمات پرکاربرد</div>
        <div class="chips" id="serviceChips"></div>

        <div id="selectedServiceBox" class="selected-box">
          <div class="empty-box">یک خدمت را انتخاب کن</div>
        </div>

        <div class="section-title">ثبت‌های امروز</div>
        <div class="list-box" id="todayItems">
          <div class="empty-list">هنوز چیزی ثبت نشده</div>
        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 95px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{
    display:none;
  }
  .page.active{
    display:block;
  }
  .page-title{
    margin-bottom:16px;
  }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }
  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{
    background:linear-gradient(135deg,#0f172a,#1e293b);
  }
  .summary-card.green{
    background:linear-gradient(135deg,#16a34a,#15803d);
  }
  .search-box{
    margin-bottom:14px;
  }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }
  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }
  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{
    display:none;
  }
  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }
  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }
  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }
  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }
  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }
  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }
  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }
  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }
  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }
  .wallet-card small,.mini-card small,.manager-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong{
    font-size:17px;
    font-weight:800;
  }
  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    grid-template-columns:repeat(4,1fr);
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:86px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  @media (max-width:360px){
    .factory-phone{ padding:16px 12px 95px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .manager-grid{ grid-template-columns:1fr 1fr; }
  }
</style>

<script>
(function(){
  const services = [
    { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 },
    { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 },
    { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 },
    { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 },
    { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 },
    { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 }
  ];

  let selectedService = null;
  let currentQty = 1;

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان" }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const chipsBox = document.getElementById("serviceChips");
  const selectedServiceBox = document.getElementById("selectedServiceBox");
  const todayItems = document.getElementById("todayItems");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const serviceSearch = document.getElementById("serviceSearch");
  const toast = document.getElementById("toast");

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getFilteredServices(){
    const q = normalizeText(serviceSearch.value.trim());
    if(!q) return services;
    return services.filter(s => normalizeText(s.name).includes(q));
  }

  function renderChips(list = services){
    chipsBox.innerHTML = "";
    list.forEach(service => {
      const btn = document.createElement("button");
      btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : "");
      btn.textContent = service.name;
      btn.onclick = function(){
        selectedService = service;
        currentQty = 1;
        renderChips(getFilteredServices());
        renderSelectedService();
      };
      chipsBox.appendChild(btn);
    });
  }

  function renderSelectedService(){
    if(!selectedService){
      selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`;
      return;
    }

    selectedServiceBox.innerHTML = `
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>${selectedService.name}</h3>
            <p>قیمت واحد: ${money(selectedService.price)}</p>
          </div>
          <div class="price-badge">${money(selectedService.price * currentQty)}</div>
        </div>

        <div class="counter">
          <button type="button" id="minusQty">−</button>
          <input type="number" id="qtyInput" min="1" value="${currentQty}">
          <button type="button" id="plusQty">+</button>
        </div>

        <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button>
      </div>
    `;

    document.getElementById("minusQty").onclick = function(){
      currentQty = Math.max(1, currentQty - 1);
      renderSelectedService();
    };

    document.getElementById("plusQty").onclick = function(){
      currentQty++;
      renderSelectedService();
    };

    document.getElementById("qtyInput").oninput = function(e){
      currentQty = Math.max(1, parseInt(e.target.value || "1"));
      renderSelectedService();
    };

    document.getElementById("addTodayBtn").onclick = function(){
      entries.unshift({
        id: Date.now(),
        serviceId: selectedService.id,
        name: selectedService.name,
        price: selectedService.price,
        qty: currentQty,
        status: "pending",
        worker: "عرفان"
      });
      currentQty = 1;
      renderAll();
      showToast("ثبت جدید اضافه شد");
    };
  }

  function renderTodayItems(){
    if(entries.length === 0){
      todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`;
      return;
    }

    todayItems.innerHTML = "";
    entries.forEach((item, index) => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(item.qty * item.price)}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      `;
      row.querySelector(".btn-remove").onclick = function(){
        entries.splice(index, 1);
        renderAll();
        showToast("آیتم حذف شد");
      };
      todayItems.appendChild(row);
    });
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(item.qty * item.price)}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + (item.qty * item.price), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + (item.qty * item.price), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + (item.qty * item.price), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد | ${money(item.qty * item.price)}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + item.qty * item.price, 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += item.qty * item.price;
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderRegisterSummary(){
    const totalQty = entries.reduce((sum, item) => sum + item.qty, 0);
    const totalPrice = entries.reduce((sum, item) => sum + item.qty * item.price, 0);

    document.getElementById("regTotalQty").textContent = toFa(totalQty);
    document.getElementById("regTotalPrice").textContent = money(totalPrice);
  }

  function renderAll(){
    renderChips(getFilteredServices());
    renderSelectedService();
    renderTodayItems();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderRegisterSummary();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function(){
    renderChips(getFilteredServices());
  });

  renderAll();
})();
</script>
۲۲ تنوع
TEXT - 2026-05-05 23:25:31
if (!defined('ABSPATH')) exit; /** * فقط ادمین */ function qv_is_admin_user() { return current_user_can('manage_woocommerce') || current_user_can('administrator'); } /** * لیبل attribute */ function qv_get_attribute_label_safe($name, $product = null) { if (function_exists('wc_attribute_label')) { $label = wc_attribute_label($name, $product); if (!empty($label)) return $label; } if (strpos($name, 'pa_') === 0) { $name = str_replace('pa_', '', $name); } return ucfirst(str_replace(array('-', '_'), ' ', $name)); } /** * متن خوانا برای option */ function qv_get_readable_option_label($attribute_name, $option_value) { if ($option_value === '' || $option_value === null) { return ''; } if (taxonomy_exists($attribute_name)) { $term = get_term_by('slug', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } $term = get_term_by('name', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } } $decoded = rawurldecode($option_value); $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8'); return $decoded; } /** * همه attributeهای قابل انتخاب * - هم attributeهای روی خود محصول * - هم همه attributeهای سراسری ووکامرس */ function qv_get_all_selectable_attributes($product) { $result = array(); $map = array(); /** * 1) اول attributeهای خود محصول */ $product_attributes = $product->get_attributes(); if (!empty($product_attributes)) { foreach ($product_attributes as $attribute_key => $attribute_obj) { if (!is_a($attribute_obj, 'WC_Product_Attribute')) { continue; } $attribute_name = $attribute_obj->get_name(); $label = qv_get_attribute_label_safe($attribute_name, $product); $options = array(); if ($attribute_obj->is_taxonomy()) { $terms = wc_get_product_terms($product->get_id(), $attribute_name, array('fields' => 'all')); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => $term->slug, 'label' => $term->name, ); } } } else { $raw_options = $attribute_obj->get_options(); if (!empty($raw_options)) { foreach ($raw_options as $opt) { if ($opt === '' || $opt === null) continue; $options[] = array( 'value' => $opt, 'label' => qv_get_readable_option_label($attribute_name, $opt), ); } } } if (!isset($map[$attribute_name])) { $map[$attribute_name] = array( 'name' => $attribute_name, 'label' => $label, 'options' => array(), ); } foreach ($options as $opt) { $map[$attribute_name]['options'][(string)$opt['value']] = $opt; } } } /** * 2) همه attributeهای سراسری ووکامرس */ $global_attributes = function_exists('wc_get_attribute_taxonomies') ? wc_get_attribute_taxonomies() : array(); if (!empty($global_attributes)) { foreach ($global_attributes as $ga) { if (empty($ga->attribute_name)) continue; $taxonomy = wc_attribute_taxonomy_name($ga->attribute_name); if (!taxonomy_exists($taxonomy)) continue; $label = !empty($ga->attribute_label) ? $ga->attribute_label : qv_get_attribute_label_safe($taxonomy, $product); if (!isset($map[$taxonomy])) { $map[$taxonomy] = array( 'name' => $taxonomy, 'label' => $label, 'options' => array(), ); } $terms = get_terms(array( 'taxonomy' => $taxonomy, 'hide_empty' => false, )); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $map[$taxonomy]['options'][(string)$term->slug] = array( 'value' => $term->slug, 'label' => $term->name, ); } } } } foreach ($map as $attribute_name => $item) { if (!empty($item['options'])) { $item['options'] = array_values($item['options']); $result[] = $item; } } return $result; } /** * تبدیل محصول به variable */ function qv_ensure_variable_product($product_id) { $product = wc_get_product($product_id); if (!$product) return false; if ($product->is_type('variable')) { return true; } wp_set_object_terms($product_id, 'variable', 'product_type'); clean_post_cache($product_id); $product = wc_get_product($product_id); return ($product && $product->is_type('variable')); } /** * افزودن attribute به محصول اگر نبود */ function qv_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') { $product = wc_get_product($product_id); if (!$product) return false; $attributes = $product->get_attributes(); if (isset($attributes[$attribute_name])) { $attr_obj = $attributes[$attribute_name]; if (is_a($attr_obj, 'WC_Product_Attribute')) { $attr_obj->set_visible(true); $attr_obj->set_variation(true); if (!$attr_obj->is_taxonomy() && $attribute_value !== '') { $options = $attr_obj->get_options(); if (!in_array($attribute_value, $options, true)) { $options[] = $attribute_value; $attr_obj->set_options($options); } } $attributes[$attribute_name] = $attr_obj; $product->set_attributes($attributes); $product->save(); } return true; } $new_attr = new WC_Product_Attribute(); if (taxonomy_exists($attribute_name)) { $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0; $new_attr->set_id($taxonomy_id); $new_attr->set_name($attribute_name); $new_attr->set_options(array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } else { $new_attr->set_id(0); $new_attr->set_name($attribute_name); $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } $attributes[$attribute_name] = $new_attr; $product->set_attributes($attributes); $product->save(); return true; } /** * variation تکراری */ function qv_variation_exists($product_id, $variation_attributes) { $children = get_posts(array( 'post_parent' => $product_id, 'post_type' => 'product_variation', 'post_status' => array('publish', 'private'), 'numberposts' => -1, 'fields' => 'ids', )); if (empty($children)) return false; foreach ($children as $variation_id) { $same = true; foreach ($variation_attributes as $key => $value) { $existing = get_post_meta($variation_id, $key, true); if ((string)$existing !== (string)$value) { $same = false; break; } } if ($same) { return true; } } return false; } /** * فرم */ function qv_render_quick_variation_form() { if (!is_product()) return; if (!qv_is_admin_user()) return; global $product; if (!$product || !is_a($product, 'WC_Product')) return; $attributes = qv_get_all_selectable_attributes($product); if (empty($attributes)) return; ?> <div class="qv-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;"> <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3> <form method="post" class="qv-quick-variation-form" autocomplete="off" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <?php wp_nonce_field('qv_quick_variation_action', 'qv_quick_variation_nonce'); ?> <input type="hidden" name="qv_product_id" value="<?php echo esc_attr($product->get_id()); ?>"> <div> <label style="display:block;margin-bottom:6px;">ویژگی اول</label> <select name="qv_attr1" id="qv_attr1_custom" style="width:100%;padding:8px;"> <option value="">انتخاب ویژگی</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label> <select name="qv_val1" id="qv_val1_custom" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div> <label style="display:block;margin-bottom:6px;">ویژگی دوم</label> <select name="qv_attr2" id="qv_attr2_custom" style="width:100%;padding:8px;"> <option value="">بدون ویژگی دوم</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label> <select name="qv_val2" id="qv_val2_custom" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div style="grid-column:1/-1;"> <label style="display:block;margin-bottom:6px;">قیمت</label> <input type="number" step="0.01" min="0" name="qv_price" required style="width:100%;padding:8px;"> </div> <div style="grid-column:1/-1;"> <button type="submit" name="qv_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;"> افزودن تنوع </button> </div> </form> </div> <script> (function(){ var attributes = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>; var attr1 = document.getElementById('qv_attr1_custom'); var val1 = document.getElementById('qv_val1_custom'); var attr2 = document.getElementById('qv_attr2_custom'); var val2 = document.getElementById('qv_val2_custom'); if (!attr1 || !val1 || !attr2 || !val2) return; function findAttribute(name) { for (var i = 0; i < attributes.length; i++) { if (attributes[i].name === name) return attributes[i]; } return null; } function fillValues(attrSelect, valueSelect) { var attrName = attrSelect.value; var previousValue = valueSelect.value || ''; valueSelect.innerHTML = ''; if (!attrName) { var p = document.createElement('option'); p.value = ''; p.textContent = 'ابتدا ویژگی را انتخاب کنید'; valueSelect.appendChild(p); return; } var data = findAttribute(attrName); var first = document.createElement('option'); first.value = ''; first.textContent = 'انتخاب مقدار'; valueSelect.appendChild(first); var any = document.createElement('option'); any.value = '__any__'; any.textContent = 'همه موارد'; valueSelect.appendChild(any); if (data && data.options) { data.options.forEach(function(opt){ var option = document.createElement('option'); option.value = opt.value; option.textContent = opt.label; valueSelect.appendChild(option); }); } if (previousValue) { var exists = false; for (var i = 0; i < valueSelect.options.length; i++) { if (valueSelect.options[i].value === previousValue) { exists = true; break; } } valueSelect.value = exists ? previousValue : ''; } } attr1.addEventListener('change', function(e){ e.stopPropagation(); fillValues(attr1, val1); if (attr2.value && attr2.value === attr1.value) { attr2.value = ''; fillValues(attr2, val2); } }, true); attr2.addEventListener('change', function(e){ e.stopPropagation(); if (attr1.value && attr2.value && attr1.value === attr2.value) { alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.'); attr2.value = ''; } fillValues(attr2, val2); }, true); val1.addEventListener('change', function(e){ e.stopPropagation(); }, true); val2.addEventListener('change', function(e){ e.stopPropagation(); }, true); attr1.addEventListener('click', function(e){ e.stopPropagation(); }, true); attr2.addEventListener('click', function(e){ e.stopPropagation(); }, true); val1.addEventListener('click', function(e){ e.stopPropagation(); }, true); val2.addEventListener('click', function(e){ e.stopPropagation(); }, true); })(); </script> <?php } add_action('woocommerce_after_single_product_summary', 'qv_render_quick_variation_form', 5); /** * ثبت فرم */ function qv_handle_quick_variation_submit() { if (!isset($_POST['qv_quick_variation_submit'])) return; if (!qv_is_admin_user()) return; if (!isset($_POST['qv_quick_variation_nonce']) || !wp_verify_nonce($_POST['qv_quick_variation_nonce'], 'qv_quick_variation_action')) { return; } $product_id = isset($_POST['qv_product_id']) ? absint($_POST['qv_product_id']) : 0; $attr1 = isset($_POST['qv_attr1']) ? wc_clean(wp_unslash($_POST['qv_attr1'])) : ''; $val1 = isset($_POST['qv_val1']) ? wc_clean(wp_unslash($_POST['qv_val1'])) : ''; $attr2 = isset($_POST['qv_attr2']) ? wc_clean(wp_unslash($_POST['qv_attr2'])) : ''; $val2 = isset($_POST['qv_val2']) ? wc_clean(wp_unslash($_POST['qv_val2'])) : ''; $price = isset($_POST['qv_price']) ? wc_format_decimal(wp_unslash($_POST['qv_price'])) : ''; if (!$product_id || !$attr1 || $val1 === '' || $price === '') { wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error'); return; } if ($attr2 && !$val2 && $val2 !== '__any__') { wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error'); return; } if ($attr1 && $attr2 && $attr1 === $attr2) { wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error'); return; } if ($val1 === '__any__' && $val2 === '__any__') { wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان همه موارد را انتخاب کرد.', 'error'); return; } if (!qv_ensure_variable_product($product_id)) { wc_add_notice('تبدیل محصول به variable ناموفق بود.', 'error'); return; } qv_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : ''); if ($attr2) { qv_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : ''); } $variation_attributes = array( 'attribute_' . $attr1 => ($val1 === '__any__' ? '' : $val1), ); if ($attr2) { $variation_attributes['attribute_' . $attr2] = ($val2 === '__any__' ? '' : $val2); } if (qv_variation_exists($product_id, $variation_attributes)) { wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error'); return; } $variation_post = array( 'post_title' => 'Product variation', 'post_name' => 'product-' . $product_id . '-variation', 'post_status' => 'publish', 'post_parent' => $product_id, 'post_type' => 'product_variation', 'guid' => home_url('/?product_variation=product-' . $product_id . '-variation'), ); $variation_id = wp_insert_post($variation_post); if (!$variation_id || is_wp_error($variation_id)) { wc_add_notice('ساخت variation ناموفق بود.', 'error'); return; } foreach ($variation_attributes as $meta_key => $meta_value) { update_post_meta($variation_id, $meta_key, $meta_value); } update_post_meta($variation_id, '_regular_price', $price); update_post_meta($variation_id, '_price', $price); $variation = new WC_Product_Variation($variation_id); $variation->set_parent_id($product_id); $variation->set_regular_price($price); $variation->set_price($price); $set_attrs = array( $attr1 => ($val1 === '__any__' ? '' : $val1), ); if ($attr2) { $set_attrs[$attr2] = ($val2 === '__any__' ? '' : $val2); } $variation->set_attributes($set_attrs); $variation->save(); WC_Product_Variable::sync($product_id); wc_delete_product_transients($product_id); wc_add_notice('تنوع جدید با موفقیت ساخته شد.', 'success'); } add_action('init', 'qv_handle_quick_variation_submit');
if (!defined('ABSPATH')) exit;

/**
 * فقط ادمین
 */
function qv_is_admin_user() {
    return current_user_can('manage_woocommerce') || current_user_can('administrator');
}

/**
 * لیبل attribute
 */
function qv_get_attribute_label_safe($name, $product = null) {
    if (function_exists('wc_attribute_label')) {
        $label = wc_attribute_label($name, $product);
        if (!empty($label)) return $label;
    }

    if (strpos($name, 'pa_') === 0) {
        $name = str_replace('pa_', '', $name);
    }

    return ucfirst(str_replace(array('-', '_'), ' ', $name));
}

/**
 * متن خوانا برای option
 */
function qv_get_readable_option_label($attribute_name, $option_value) {
    if ($option_value === '' || $option_value === null) {
        return '';
    }

    if (taxonomy_exists($attribute_name)) {
        $term = get_term_by('slug', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }

        $term = get_term_by('name', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }
    }

    $decoded = rawurldecode($option_value);
    $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8');
    return $decoded;
}

/**
 * همه attributeهای قابل انتخاب
 * - هم attributeهای روی خود محصول
 * - هم همه attributeهای سراسری ووکامرس
 */
function qv_get_all_selectable_attributes($product) {
    $result = array();
    $map = array();

    /**
     * 1) اول attributeهای خود محصول
     */
    $product_attributes = $product->get_attributes();

    if (!empty($product_attributes)) {
        foreach ($product_attributes as $attribute_key => $attribute_obj) {
            if (!is_a($attribute_obj, 'WC_Product_Attribute')) {
                continue;
            }

            $attribute_name = $attribute_obj->get_name();
            $label = qv_get_attribute_label_safe($attribute_name, $product);
            $options = array();

            if ($attribute_obj->is_taxonomy()) {
                $terms = wc_get_product_terms($product->get_id(), $attribute_name, array('fields' => 'all'));

                if (!empty($terms) && !is_wp_error($terms)) {
                    foreach ($terms as $term) {
                        $options[] = array(
                            'value' => $term->slug,
                            'label' => $term->name,
                        );
                    }
                }
            } else {
                $raw_options = $attribute_obj->get_options();

                if (!empty($raw_options)) {
                    foreach ($raw_options as $opt) {
                        if ($opt === '' || $opt === null) continue;

                        $options[] = array(
                            'value' => $opt,
                            'label' => qv_get_readable_option_label($attribute_name, $opt),
                        );
                    }
                }
            }

            if (!isset($map[$attribute_name])) {
                $map[$attribute_name] = array(
                    'name'    => $attribute_name,
                    'label'   => $label,
                    'options' => array(),
                );
            }

            foreach ($options as $opt) {
                $map[$attribute_name]['options'][(string)$opt['value']] = $opt;
            }
        }
    }

    /**
     * 2) همه attributeهای سراسری ووکامرس
     */
    $global_attributes = function_exists('wc_get_attribute_taxonomies') ? wc_get_attribute_taxonomies() : array();

    if (!empty($global_attributes)) {
        foreach ($global_attributes as $ga) {
            if (empty($ga->attribute_name)) continue;

            $taxonomy = wc_attribute_taxonomy_name($ga->attribute_name);
            if (!taxonomy_exists($taxonomy)) continue;

            $label = !empty($ga->attribute_label) ? $ga->attribute_label : qv_get_attribute_label_safe($taxonomy, $product);

            if (!isset($map[$taxonomy])) {
                $map[$taxonomy] = array(
                    'name'    => $taxonomy,
                    'label'   => $label,
                    'options' => array(),
                );
            }

            $terms = get_terms(array(
                'taxonomy'   => $taxonomy,
                'hide_empty' => false,
            ));

            if (!empty($terms) && !is_wp_error($terms)) {
                foreach ($terms as $term) {
                    $map[$taxonomy]['options'][(string)$term->slug] = array(
                        'value' => $term->slug,
                        'label' => $term->name,
                    );
                }
            }
        }
    }

    foreach ($map as $attribute_name => $item) {
        if (!empty($item['options'])) {
            $item['options'] = array_values($item['options']);
            $result[] = $item;
        }
    }

    return $result;
}

/**
 * تبدیل محصول به variable
 */
function qv_ensure_variable_product($product_id) {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    if ($product->is_type('variable')) {
        return true;
    }

    wp_set_object_terms($product_id, 'variable', 'product_type');
    clean_post_cache($product_id);

    $product = wc_get_product($product_id);
    return ($product && $product->is_type('variable'));
}

/**
 * افزودن attribute به محصول اگر نبود
 */
function qv_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    $attributes = $product->get_attributes();

    if (isset($attributes[$attribute_name])) {
        $attr_obj = $attributes[$attribute_name];

        if (is_a($attr_obj, 'WC_Product_Attribute')) {
            $attr_obj->set_visible(true);
            $attr_obj->set_variation(true);

            if (!$attr_obj->is_taxonomy() && $attribute_value !== '') {
                $options = $attr_obj->get_options();
                if (!in_array($attribute_value, $options, true)) {
                    $options[] = $attribute_value;
                    $attr_obj->set_options($options);
                }
            }

            $attributes[$attribute_name] = $attr_obj;
            $product->set_attributes($attributes);
            $product->save();
        }

        return true;
    }

    $new_attr = new WC_Product_Attribute();

    if (taxonomy_exists($attribute_name)) {
        $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0;
        $new_attr->set_id($taxonomy_id);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options(array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    } else {
        $new_attr->set_id(0);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    }

    $attributes[$attribute_name] = $new_attr;
    $product->set_attributes($attributes);
    $product->save();

    return true;
}

/**
 * variation تکراری
 */
function qv_variation_exists($product_id, $variation_attributes) {
    $children = get_posts(array(
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'post_status' => array('publish', 'private'),
        'numberposts' => -1,
        'fields'      => 'ids',
    ));

    if (empty($children)) return false;

    foreach ($children as $variation_id) {
        $same = true;

        foreach ($variation_attributes as $key => $value) {
            $existing = get_post_meta($variation_id, $key, true);
            if ((string)$existing !== (string)$value) {
                $same = false;
                break;
            }
        }

        if ($same) {
            return true;
        }
    }

    return false;
}

/**
 * فرم
 */
function qv_render_quick_variation_form() {
    if (!is_product()) return;
    if (!qv_is_admin_user()) return;

    global $product;
    if (!$product || !is_a($product, 'WC_Product')) return;

    $attributes = qv_get_all_selectable_attributes($product);
    if (empty($attributes)) return;
    ?>
    <div class="qv-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;">
        <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3>

        <form method="post" class="qv-quick-variation-form" autocomplete="off" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
            <?php wp_nonce_field('qv_quick_variation_action', 'qv_quick_variation_nonce'); ?>
            <input type="hidden" name="qv_product_id" value="<?php echo esc_attr($product->get_id()); ?>">

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی اول</label>
                <select name="qv_attr1" id="qv_attr1_custom" style="width:100%;padding:8px;">
                    <option value="">انتخاب ویژگی</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label>
                <select name="qv_val1" id="qv_val1_custom" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی دوم</label>
                <select name="qv_attr2" id="qv_attr2_custom" style="width:100%;padding:8px;">
                    <option value="">بدون ویژگی دوم</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label>
                <select name="qv_val2" id="qv_val2_custom" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div style="grid-column:1/-1;">
                <label style="display:block;margin-bottom:6px;">قیمت</label>
                <input type="number" step="0.01" min="0" name="qv_price" required style="width:100%;padding:8px;">
            </div>

            <div style="grid-column:1/-1;">
                <button type="submit" name="qv_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;">
                    افزودن تنوع
                </button>
            </div>
        </form>
    </div>

    <script>
    (function(){
        var attributes = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;

        var attr1 = document.getElementById('qv_attr1_custom');
        var val1  = document.getElementById('qv_val1_custom');
        var attr2 = document.getElementById('qv_attr2_custom');
        var val2  = document.getElementById('qv_val2_custom');

        if (!attr1 || !val1 || !attr2 || !val2) return;

        function findAttribute(name) {
            for (var i = 0; i < attributes.length; i++) {
                if (attributes[i].name === name) return attributes[i];
            }
            return null;
        }

        function fillValues(attrSelect, valueSelect) {
            var attrName = attrSelect.value;
            var previousValue = valueSelect.value || '';

            valueSelect.innerHTML = '';

            if (!attrName) {
                var p = document.createElement('option');
                p.value = '';
                p.textContent = 'ابتدا ویژگی را انتخاب کنید';
                valueSelect.appendChild(p);
                return;
            }

            var data = findAttribute(attrName);

            var first = document.createElement('option');
            first.value = '';
            first.textContent = 'انتخاب مقدار';
            valueSelect.appendChild(first);

            var any = document.createElement('option');
            any.value = '__any__';
            any.textContent = 'همه موارد';
            valueSelect.appendChild(any);

            if (data && data.options) {
                data.options.forEach(function(opt){
                    var option = document.createElement('option');
                    option.value = opt.value;
                    option.textContent = opt.label;
                    valueSelect.appendChild(option);
                });
            }

            if (previousValue) {
                var exists = false;
                for (var i = 0; i < valueSelect.options.length; i++) {
                    if (valueSelect.options[i].value === previousValue) {
                        exists = true;
                        break;
                    }
                }
                valueSelect.value = exists ? previousValue : '';
            }
        }

        attr1.addEventListener('change', function(e){
            e.stopPropagation();
            fillValues(attr1, val1);

            if (attr2.value && attr2.value === attr1.value) {
                attr2.value = '';
                fillValues(attr2, val2);
            }
        }, true);

        attr2.addEventListener('change', function(e){
            e.stopPropagation();

            if (attr1.value && attr2.value && attr1.value === attr2.value) {
                alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.');
                attr2.value = '';
            }

            fillValues(attr2, val2);
        }, true);

        val1.addEventListener('change', function(e){
            e.stopPropagation();
        }, true);

        val2.addEventListener('change', function(e){
            e.stopPropagation();
        }, true);

        attr1.addEventListener('click', function(e){ e.stopPropagation(); }, true);
        attr2.addEventListener('click', function(e){ e.stopPropagation(); }, true);
        val1.addEventListener('click', function(e){ e.stopPropagation(); }, true);
        val2.addEventListener('click', function(e){ e.stopPropagation(); }, true);
    })();
    </script>
    <?php
}
add_action('woocommerce_after_single_product_summary', 'qv_render_quick_variation_form', 5);

/**
 * ثبت فرم
 */
function qv_handle_quick_variation_submit() {
    if (!isset($_POST['qv_quick_variation_submit'])) return;
    if (!qv_is_admin_user()) return;

    if (!isset($_POST['qv_quick_variation_nonce']) || !wp_verify_nonce($_POST['qv_quick_variation_nonce'], 'qv_quick_variation_action')) {
        return;
    }

    $product_id = isset($_POST['qv_product_id']) ? absint($_POST['qv_product_id']) : 0;
    $attr1      = isset($_POST['qv_attr1']) ? wc_clean(wp_unslash($_POST['qv_attr1'])) : '';
    $val1       = isset($_POST['qv_val1']) ? wc_clean(wp_unslash($_POST['qv_val1'])) : '';
    $attr2      = isset($_POST['qv_attr2']) ? wc_clean(wp_unslash($_POST['qv_attr2'])) : '';
    $val2       = isset($_POST['qv_val2']) ? wc_clean(wp_unslash($_POST['qv_val2'])) : '';
    $price      = isset($_POST['qv_price']) ? wc_format_decimal(wp_unslash($_POST['qv_price'])) : '';

    if (!$product_id || !$attr1 || $val1 === '' || $price === '') {
        wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error');
        return;
    }

    if ($attr2 && !$val2 && $val2 !== '__any__') {
        wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error');
        return;
    }

    if ($attr1 && $attr2 && $attr1 === $attr2) {
        wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error');
        return;
    }

    if ($val1 === '__any__' && $val2 === '__any__') {
        wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان همه موارد را انتخاب کرد.', 'error');
        return;
    }

    if (!qv_ensure_variable_product($product_id)) {
        wc_add_notice('تبدیل محصول به variable ناموفق بود.', 'error');
        return;
    }

    qv_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : '');
    if ($attr2) {
        qv_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : '');
    }

    $variation_attributes = array(
        'attribute_' . $attr1 => ($val1 === '__any__' ? '' : $val1),
    );

    if ($attr2) {
        $variation_attributes['attribute_' . $attr2] = ($val2 === '__any__' ? '' : $val2);
    }

    if (qv_variation_exists($product_id, $variation_attributes)) {
        wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error');
        return;
    }

    $variation_post = array(
        'post_title'  => 'Product variation',
        'post_name'   => 'product-' . $product_id . '-variation',
        'post_status' => 'publish',
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'guid'        => home_url('/?product_variation=product-' . $product_id . '-variation'),
    );

    $variation_id = wp_insert_post($variation_post);

    if (!$variation_id || is_wp_error($variation_id)) {
        wc_add_notice('ساخت variation ناموفق بود.', 'error');
        return;
    }

    foreach ($variation_attributes as $meta_key => $meta_value) {
        update_post_meta($variation_id, $meta_key, $meta_value);
    }

    update_post_meta($variation_id, '_regular_price', $price);
    update_post_meta($variation_id, '_price', $price);

    $variation = new WC_Product_Variation($variation_id);
    $variation->set_parent_id($product_id);
    $variation->set_regular_price($price);
    $variation->set_price($price);

    $set_attrs = array(
        $attr1 => ($val1 === '__any__' ? '' : $val1),
    );

    if ($attr2) {
        $set_attrs[$attr2] = ($val2 === '__any__' ? '' : $val2);
    }

    $variation->set_attributes($set_attrs);
    $variation->save();

    WC_Product_Variable::sync($product_id);
    wc_delete_product_transients($product_id);

    wc_add_notice('تنوع جدید با موفقیت ساخته شد.', 'success');
}
add_action('init', 'qv_handle_quick_variation_submit');
تنوع ۲۱
TEXT - 2026-05-05 23:22:49
if (!defined('ABSPATH')) exit; /** * فقط ادمین */ function qv_is_admin_user() { return current_user_can('manage_woocommerce') || current_user_can('administrator'); } /** * گرفتن لیبل attribute */ function qv_get_attribute_label_safe($name, $product = null) { if (function_exists('wc_attribute_label')) { $label = wc_attribute_label($name, $product); if (!empty($label)) { return $label; } } if (strpos($name, 'pa_') === 0) { $name = str_replace('pa_', '', $name); } return ucfirst(str_replace(array('-', '_'), ' ', $name)); } /** * متن قابل نمایش برای option */ function qv_get_readable_option_label($attribute_name, $option_value) { if ($option_value === '' || $option_value === null) { return ''; } if (taxonomy_exists($attribute_name)) { $term = get_term_by('slug', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } $term = get_term_by('name', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } } $decoded = rawurldecode($option_value); $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8'); return $decoded; } /** * گرفتن همه attributeهای محصول * شامل global و local */ function qv_get_all_product_attributes($product) { $result = array(); $attributes = $product->get_attributes(); if (empty($attributes)) { return $result; } foreach ($attributes as $attribute_key => $attribute_obj) { if (!is_a($attribute_obj, 'WC_Product_Attribute')) { continue; } $attribute_name = $attribute_obj->get_name(); $label = qv_get_attribute_label_safe($attribute_name, $product); $options = array(); if ($attribute_obj->is_taxonomy()) { $terms = wc_get_product_terms($product->get_id(), $attribute_name, array('fields' => 'all')); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => $term->slug, 'label' => $term->name, ); } } } else { $raw_options = $attribute_obj->get_options(); if (!empty($raw_options)) { foreach ($raw_options as $opt) { if ($opt === '' || $opt === null) continue; $options[] = array( 'value' => $opt, 'label' => qv_get_readable_option_label($attribute_name, $opt), ); } } } if (!empty($options)) { $unique = array(); $clean_options = array(); foreach ($options as $opt) { $key = (string) $opt['value']; if (isset($unique[$key])) continue; $unique[$key] = true; $clean_options[] = $opt; } $result[] = array( 'name' => $attribute_name, 'label' => $label, 'options' => array_values($clean_options), ); } } return $result; } /** * تبدیل محصول ساده به variable */ function qv_ensure_variable_product($product_id) { $product = wc_get_product($product_id); if (!$product) return false; if ($product->is_type('variable')) { return true; } wp_set_object_terms($product_id, 'variable', 'product_type'); clean_post_cache($product_id); $product = wc_get_product($product_id); return ($product && $product->is_type('variable')); } /** * افزودن attribute به محصول اگر نبود */ function qv_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') { $product = wc_get_product($product_id); if (!$product) return false; $attributes = $product->get_attributes(); if (isset($attributes[$attribute_name])) { $attr_obj = $attributes[$attribute_name]; if (is_a($attr_obj, 'WC_Product_Attribute')) { $attr_obj->set_visible(true); $attr_obj->set_variation(true); if (!$attr_obj->is_taxonomy() && $attribute_value !== '') { $options = $attr_obj->get_options(); if (!in_array($attribute_value, $options, true)) { $options[] = $attribute_value; $attr_obj->set_options($options); } } $attributes[$attribute_name] = $attr_obj; $product->set_attributes($attributes); $product->save(); } return true; } $new_attr = new WC_Product_Attribute(); if (taxonomy_exists($attribute_name)) { $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0; $new_attr->set_id($taxonomy_id); $new_attr->set_name($attribute_name); $new_attr->set_options(array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } else { $new_attr->set_id(0); $new_attr->set_name($attribute_name); $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } $attributes[$attribute_name] = $new_attr; $product->set_attributes($attributes); $product->save(); return true; } /** * بررسی variation تکراری */ function qv_variation_exists($product_id, $variation_attributes) { $children = get_posts(array( 'post_parent' => $product_id, 'post_type' => 'product_variation', 'post_status' => array('publish', 'private'), 'numberposts' => -1, 'fields' => 'ids', )); if (empty($children)) return false; foreach ($children as $variation_id) { $existing = array(); foreach ($variation_attributes as $key => $value) { $existing[$key] = get_post_meta($variation_id, $key, true); } $same = true; foreach ($variation_attributes as $key => $value) { if ((string) $existing[$key] !== (string) $value) { $same = false; break; } } if ($same) { return true; } } return false; } /** * فرم افزودن سریع variation */ function qv_render_quick_variation_form() { if (!is_product()) return; if (!qv_is_admin_user()) return; global $product; if (!$product || !is_a($product, 'WC_Product')) return; $attributes = qv_get_all_product_attributes($product); if (empty($attributes)) return; ?> <div class="qv-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;"> <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3> <form method="post" class="qv-quick-variation-form" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <?php wp_nonce_field('qv_quick_variation_action', 'qv_quick_variation_nonce'); ?> <input type="hidden" name="qv_product_id" value="<?php echo esc_attr($product->get_id()); ?>"> <div> <label style="display:block;margin-bottom:6px;">ویژگی اول</label> <select name="qv_attr1" id="qv_attr1" style="width:100%;padding:8px;"> <option value="">انتخاب ویژگی</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label> <select name="qv_val1" id="qv_val1" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div> <label style="display:block;margin-bottom:6px;">ویژگی دوم</label> <select name="qv_attr2" id="qv_attr2" style="width:100%;padding:8px;"> <option value="">بدون ویژگی دوم</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label> <select name="qv_val2" id="qv_val2" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div style="grid-column:1/-1;"> <label style="display:block;margin-bottom:6px;">قیمت</label> <input type="number" step="0.01" min="0" name="qv_price" required style="width:100%;padding:8px;"> </div> <div style="grid-column:1/-1;"> <button type="submit" name="qv_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;"> افزودن تنوع </button> </div> </form> </div> <script> (function(){ var attributes = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>; var attr1 = document.getElementById('qv_attr1'); var val1 = document.getElementById('qv_val1'); var attr2 = document.getElementById('qv_attr2'); var val2 = document.getElementById('qv_val2'); if (!attr1 || !val1 || !attr2 || !val2) return; function getAttributeData(name) { for (var i = 0; i < attributes.length; i++) { if (attributes[i].name === name) return attributes[i]; } return null; } function fillValues(attrSelect, valueSelect) { var attrName = attrSelect.value; valueSelect.innerHTML = ''; if (!attrName) { var op = document.createElement('option'); op.value = ''; op.textContent = 'ابتدا ویژگی را انتخاب کنید'; valueSelect.appendChild(op); return; } var attrData = getAttributeData(attrName); var first = document.createElement('option'); first.value = ''; first.textContent = 'انتخاب مقدار'; valueSelect.appendChild(first); var any = document.createElement('option'); any.value = '__any__'; any.textContent = 'همه موارد'; valueSelect.appendChild(any); if (attrData && attrData.options) { attrData.options.forEach(function(opt){ var option = document.createElement('option'); option.value = opt.value; option.textContent = opt.label; valueSelect.appendChild(option); }); } } attr1.addEventListener('change', function(){ fillValues(attr1, val1); if (attr2.value && attr2.value === attr1.value) { attr2.value = ''; fillValues(attr2, val2); } }); attr2.addEventListener('change', function(){ if (attr1.value && attr2.value && attr1.value === attr2.value) { alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.'); attr2.value = ''; } fillValues(attr2, val2); }); })(); </script> <?php } add_action('woocommerce_single_product_summary', 'qv_render_quick_variation_form', 35); /** * ثبت فرم */ function qv_handle_quick_variation_submit() { if (!isset($_POST['qv_quick_variation_submit'])) return; if (!qv_is_admin_user()) return; if (!isset($_POST['qv_quick_variation_nonce']) || !wp_verify_nonce($_POST['qv_quick_variation_nonce'], 'qv_quick_variation_action')) { return; } $product_id = isset($_POST['qv_product_id']) ? absint($_POST['qv_product_id']) : 0; $attr1 = isset($_POST['qv_attr1']) ? wc_clean(wp_unslash($_POST['qv_attr1'])) : ''; $val1 = isset($_POST['qv_val1']) ? wc_clean(wp_unslash($_POST['qv_val1'])) : ''; $attr2 = isset($_POST['qv_attr2']) ? wc_clean(wp_unslash($_POST['qv_attr2'])) : ''; $val2 = isset($_POST['qv_val2']) ? wc_clean(wp_unslash($_POST['qv_val2'])) : ''; $price = isset($_POST['qv_price']) ? wc_format_decimal(wp_unslash($_POST['qv_price'])) : ''; if (!$product_id || !$attr1 || $val1 === '' || $price === '') { wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error'); return; } if ($attr2 && !$val2 && $val2 !== '__any__') { wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error'); return; } if ($attr1 && $attr2 && $attr1 === $attr2) { wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error'); return; } if ($val1 === '__any__' && $val2 === '__any__') { wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان همه موارد را انتخاب کرد.', 'error'); return; } if (!qv_ensure_variable_product($product_id)) { wc_add_notice('تبدیل محصول به variable ناموفق بود.', 'error'); return; } qv_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : ''); if ($attr2) { qv_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : ''); } $variation_attributes = array( 'attribute_' . $attr1 => ($val1 === '__any__' ? '' : $val1), ); if ($attr2) { $variation_attributes['attribute_' . $attr2] = ($val2 === '__any__' ? '' : $val2); } if (qv_variation_exists($product_id, $variation_attributes)) { wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error'); return; } $variation_post = array( 'post_title' => 'Product variation', 'post_name' => 'product-' . $product_id . '-variation', 'post_status' => 'publish', 'post_parent' => $product_id, 'post_type' => 'product_variation', 'guid' => home_url('/?product_variation=product-' . $product_id . '-variation'), ); $variation_id = wp_insert_post($variation_post); if (!$variation_id || is_wp_error($variation_id)) { wc_add_notice('ساخت variation ناموفق بود.', 'error'); return; } foreach ($variation_attributes as $meta_key => $meta_value) { update_post_meta($variation_id, $meta_key, $meta_value); } update_post_meta($variation_id, '_regular_price', $price); update_post_meta($variation_id, '_price', $price); $variation = new WC_Product_Variation($variation_id); $variation->set_parent_id($product_id); $variation->set_regular_price($price); $variation->set_price($price); $set_attrs = array( $attr1 => ($val1 === '__any__' ? '' : $val1), ); if ($attr2) { $set_attrs[$attr2] = ($val2 === '__any__' ? '' : $val2); } $variation->set_attributes($set_attrs); $variation->save(); WC_Product_Variable::sync($product_id); wc_delete_product_transients($product_id); wc_add_notice('تنوع جدید با موفقیت ساخته شد.', 'success'); } add_action('init', 'qv_handle_quick_variation_submit');
if (!defined('ABSPATH')) exit;

/**
 * فقط ادمین
 */
function qv_is_admin_user() {
    return current_user_can('manage_woocommerce') || current_user_can('administrator');
}

/**
 * گرفتن لیبل attribute
 */
function qv_get_attribute_label_safe($name, $product = null) {
    if (function_exists('wc_attribute_label')) {
        $label = wc_attribute_label($name, $product);
        if (!empty($label)) {
            return $label;
        }
    }

    if (strpos($name, 'pa_') === 0) {
        $name = str_replace('pa_', '', $name);
    }

    return ucfirst(str_replace(array('-', '_'), ' ', $name));
}

/**
 * متن قابل نمایش برای option
 */
function qv_get_readable_option_label($attribute_name, $option_value) {
    if ($option_value === '' || $option_value === null) {
        return '';
    }

    if (taxonomy_exists($attribute_name)) {
        $term = get_term_by('slug', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }

        $term = get_term_by('name', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }
    }

    $decoded = rawurldecode($option_value);
    $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8');
    return $decoded;
}

/**
 * گرفتن همه attributeهای محصول
 * شامل global و local
 */
function qv_get_all_product_attributes($product) {
    $result = array();
    $attributes = $product->get_attributes();

    if (empty($attributes)) {
        return $result;
    }

    foreach ($attributes as $attribute_key => $attribute_obj) {
        if (!is_a($attribute_obj, 'WC_Product_Attribute')) {
            continue;
        }

        $attribute_name = $attribute_obj->get_name();
        $label = qv_get_attribute_label_safe($attribute_name, $product);
        $options = array();

        if ($attribute_obj->is_taxonomy()) {
            $terms = wc_get_product_terms($product->get_id(), $attribute_name, array('fields' => 'all'));

            if (!empty($terms) && !is_wp_error($terms)) {
                foreach ($terms as $term) {
                    $options[] = array(
                        'value' => $term->slug,
                        'label' => $term->name,
                    );
                }
            }
        } else {
            $raw_options = $attribute_obj->get_options();

            if (!empty($raw_options)) {
                foreach ($raw_options as $opt) {
                    if ($opt === '' || $opt === null) continue;

                    $options[] = array(
                        'value' => $opt,
                        'label' => qv_get_readable_option_label($attribute_name, $opt),
                    );
                }
            }
        }

        if (!empty($options)) {
            $unique = array();
            $clean_options = array();

            foreach ($options as $opt) {
                $key = (string) $opt['value'];
                if (isset($unique[$key])) continue;
                $unique[$key] = true;
                $clean_options[] = $opt;
            }

            $result[] = array(
                'name'    => $attribute_name,
                'label'   => $label,
                'options' => array_values($clean_options),
            );
        }
    }

    return $result;
}

/**
 * تبدیل محصول ساده به variable
 */
function qv_ensure_variable_product($product_id) {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    if ($product->is_type('variable')) {
        return true;
    }

    wp_set_object_terms($product_id, 'variable', 'product_type');
    clean_post_cache($product_id);

    $product = wc_get_product($product_id);
    return ($product && $product->is_type('variable'));
}

/**
 * افزودن attribute به محصول اگر نبود
 */
function qv_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    $attributes = $product->get_attributes();

    if (isset($attributes[$attribute_name])) {
        $attr_obj = $attributes[$attribute_name];

        if (is_a($attr_obj, 'WC_Product_Attribute')) {
            $attr_obj->set_visible(true);
            $attr_obj->set_variation(true);

            if (!$attr_obj->is_taxonomy() && $attribute_value !== '') {
                $options = $attr_obj->get_options();
                if (!in_array($attribute_value, $options, true)) {
                    $options[] = $attribute_value;
                    $attr_obj->set_options($options);
                }
            }

            $attributes[$attribute_name] = $attr_obj;
            $product->set_attributes($attributes);
            $product->save();
        }

        return true;
    }

    $new_attr = new WC_Product_Attribute();

    if (taxonomy_exists($attribute_name)) {
        $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0;
        $new_attr->set_id($taxonomy_id);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options(array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    } else {
        $new_attr->set_id(0);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    }

    $attributes[$attribute_name] = $new_attr;
    $product->set_attributes($attributes);
    $product->save();

    return true;
}

/**
 * بررسی variation تکراری
 */
function qv_variation_exists($product_id, $variation_attributes) {
    $children = get_posts(array(
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'post_status' => array('publish', 'private'),
        'numberposts' => -1,
        'fields'      => 'ids',
    ));

    if (empty($children)) return false;

    foreach ($children as $variation_id) {
        $existing = array();
        foreach ($variation_attributes as $key => $value) {
            $existing[$key] = get_post_meta($variation_id, $key, true);
        }

        $same = true;
        foreach ($variation_attributes as $key => $value) {
            if ((string) $existing[$key] !== (string) $value) {
                $same = false;
                break;
            }
        }

        if ($same) {
            return true;
        }
    }

    return false;
}

/**
 * فرم افزودن سریع variation
 */
function qv_render_quick_variation_form() {
    if (!is_product()) return;
    if (!qv_is_admin_user()) return;

    global $product;
    if (!$product || !is_a($product, 'WC_Product')) return;

    $attributes = qv_get_all_product_attributes($product);
    if (empty($attributes)) return;
    ?>
    <div class="qv-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;">
        <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3>

        <form method="post" class="qv-quick-variation-form" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
            <?php wp_nonce_field('qv_quick_variation_action', 'qv_quick_variation_nonce'); ?>
            <input type="hidden" name="qv_product_id" value="<?php echo esc_attr($product->get_id()); ?>">

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی اول</label>
                <select name="qv_attr1" id="qv_attr1" style="width:100%;padding:8px;">
                    <option value="">انتخاب ویژگی</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label>
                <select name="qv_val1" id="qv_val1" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی دوم</label>
                <select name="qv_attr2" id="qv_attr2" style="width:100%;padding:8px;">
                    <option value="">بدون ویژگی دوم</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label>
                <select name="qv_val2" id="qv_val2" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div style="grid-column:1/-1;">
                <label style="display:block;margin-bottom:6px;">قیمت</label>
                <input type="number" step="0.01" min="0" name="qv_price" required style="width:100%;padding:8px;">
            </div>

            <div style="grid-column:1/-1;">
                <button type="submit" name="qv_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;">
                    افزودن تنوع
                </button>
            </div>
        </form>
    </div>

    <script>
    (function(){
        var attributes = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;

        var attr1 = document.getElementById('qv_attr1');
        var val1  = document.getElementById('qv_val1');
        var attr2 = document.getElementById('qv_attr2');
        var val2  = document.getElementById('qv_val2');

        if (!attr1 || !val1 || !attr2 || !val2) return;

        function getAttributeData(name) {
            for (var i = 0; i < attributes.length; i++) {
                if (attributes[i].name === name) return attributes[i];
            }
            return null;
        }

        function fillValues(attrSelect, valueSelect) {
            var attrName = attrSelect.value;
            valueSelect.innerHTML = '';

            if (!attrName) {
                var op = document.createElement('option');
                op.value = '';
                op.textContent = 'ابتدا ویژگی را انتخاب کنید';
                valueSelect.appendChild(op);
                return;
            }

            var attrData = getAttributeData(attrName);

            var first = document.createElement('option');
            first.value = '';
            first.textContent = 'انتخاب مقدار';
            valueSelect.appendChild(first);

            var any = document.createElement('option');
            any.value = '__any__';
            any.textContent = 'همه موارد';
            valueSelect.appendChild(any);

            if (attrData && attrData.options) {
                attrData.options.forEach(function(opt){
                    var option = document.createElement('option');
                    option.value = opt.value;
                    option.textContent = opt.label;
                    valueSelect.appendChild(option);
                });
            }
        }

        attr1.addEventListener('change', function(){
            fillValues(attr1, val1);

            if (attr2.value && attr2.value === attr1.value) {
                attr2.value = '';
                fillValues(attr2, val2);
            }
        });

        attr2.addEventListener('change', function(){
            if (attr1.value && attr2.value && attr1.value === attr2.value) {
                alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.');
                attr2.value = '';
            }
            fillValues(attr2, val2);
        });
    })();
    </script>
    <?php
}
add_action('woocommerce_single_product_summary', 'qv_render_quick_variation_form', 35);

/**
 * ثبت فرم
 */
function qv_handle_quick_variation_submit() {
    if (!isset($_POST['qv_quick_variation_submit'])) return;
    if (!qv_is_admin_user()) return;

    if (!isset($_POST['qv_quick_variation_nonce']) || !wp_verify_nonce($_POST['qv_quick_variation_nonce'], 'qv_quick_variation_action')) {
        return;
    }

    $product_id = isset($_POST['qv_product_id']) ? absint($_POST['qv_product_id']) : 0;
    $attr1      = isset($_POST['qv_attr1']) ? wc_clean(wp_unslash($_POST['qv_attr1'])) : '';
    $val1       = isset($_POST['qv_val1']) ? wc_clean(wp_unslash($_POST['qv_val1'])) : '';
    $attr2      = isset($_POST['qv_attr2']) ? wc_clean(wp_unslash($_POST['qv_attr2'])) : '';
    $val2       = isset($_POST['qv_val2']) ? wc_clean(wp_unslash($_POST['qv_val2'])) : '';
    $price      = isset($_POST['qv_price']) ? wc_format_decimal(wp_unslash($_POST['qv_price'])) : '';

    if (!$product_id || !$attr1 || $val1 === '' || $price === '') {
        wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error');
        return;
    }

    if ($attr2 && !$val2 && $val2 !== '__any__') {
        wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error');
        return;
    }

    if ($attr1 && $attr2 && $attr1 === $attr2) {
        wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error');
        return;
    }

    if ($val1 === '__any__' && $val2 === '__any__') {
        wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان همه موارد را انتخاب کرد.', 'error');
        return;
    }

    if (!qv_ensure_variable_product($product_id)) {
        wc_add_notice('تبدیل محصول به variable ناموفق بود.', 'error');
        return;
    }

    qv_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : '');
    if ($attr2) {
        qv_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : '');
    }

    $variation_attributes = array(
        'attribute_' . $attr1 => ($val1 === '__any__' ? '' : $val1),
    );

    if ($attr2) {
        $variation_attributes['attribute_' . $attr2] = ($val2 === '__any__' ? '' : $val2);
    }

    if (qv_variation_exists($product_id, $variation_attributes)) {
        wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error');
        return;
    }

    $variation_post = array(
        'post_title'  => 'Product variation',
        'post_name'   => 'product-' . $product_id . '-variation',
        'post_status' => 'publish',
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'guid'        => home_url('/?product_variation=product-' . $product_id . '-variation'),
    );

    $variation_id = wp_insert_post($variation_post);

    if (!$variation_id || is_wp_error($variation_id)) {
        wc_add_notice('ساخت variation ناموفق بود.', 'error');
        return;
    }

    foreach ($variation_attributes as $meta_key => $meta_value) {
        update_post_meta($variation_id, $meta_key, $meta_value);
    }

    update_post_meta($variation_id, '_regular_price', $price);
    update_post_meta($variation_id, '_price', $price);

    $variation = new WC_Product_Variation($variation_id);
    $variation->set_parent_id($product_id);
    $variation->set_regular_price($price);
    $variation->set_price($price);

    $set_attrs = array(
        $attr1 => ($val1 === '__any__' ? '' : $val1),
    );

    if ($attr2) {
        $set_attrs[$attr2] = ($val2 === '__any__' ? '' : $val2);
    }

    $variation->set_attributes($set_attrs);
    $variation->save();

    WC_Product_Variable::sync($product_id);
    wc_delete_product_transients($product_id);

    wc_add_notice('تنوع جدید با موفقیت ساخته شد.', 'success');
}
add_action('init', 'qv_handle_quick_variation_submit');
کد ۲۰ تنوع
TEXT - 2026-05-05 23:18:10
if (!defined('ABSPATH')) exit; /** * فقط ادمین */ function mvx_is_admin_user() { return current_user_can('manage_woocommerce') || current_user_can('administrator'); } /** * تشخیص اینکه ویژگی taxonomy/global است یا local */ function mvx_is_taxonomy_attribute_name($name) { return taxonomy_exists($name) && strpos($name, 'pa_') === 0; } /** * گرفتن لیبل ویژگی */ function mvx_get_attribute_label_safe($name, $product = null) { if (function_exists('wc_attribute_label')) { $label = wc_attribute_label($name, $product); if (!empty($label)) return $label; } if (strpos($name, 'pa_') === 0) { return ucfirst(str_replace(array('pa_', '-', '_'), array('', ' ', ' '), $name)); } return ucfirst(str_replace(array('-', '_'), ' ', $name)); } /** * تبدیل اسلاگ یا مقدار خام به متن قابل نمایش */ function mvx_get_readable_option_label($attribute_name, $option_value) { if ($option_value === '' || $option_value === null) { return ''; } if (mvx_is_taxonomy_attribute_name($attribute_name)) { $term = get_term_by('slug', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } $term = get_term_by('name', $option_value, $attribute_name); if ($term && !is_wp_error($term)) { return $term->name; } } $decoded = rawurldecode($option_value); $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8'); return $decoded; } /** * گرفتن همه ویژگی‌های قابل انتخاب محصول * شامل global و local */ function mvx_get_all_selectable_attributes($product) { $result = array(); $attributes = $product->get_attributes(); if (empty($attributes)) { return $result; } foreach ($attributes as $attribute_name => $attribute_obj) { if (is_object($attribute_obj) && method_exists($attribute_obj, 'get_visible') && !$attribute_obj->get_visible()) { continue; } $name = is_object($attribute_obj) && method_exists($attribute_obj, 'get_name') ? $attribute_obj->get_name() : $attribute_name; $label = mvx_get_attribute_label_safe($name, $product); $options = array(); if (is_object($attribute_obj) && method_exists($attribute_obj, 'is_taxonomy') && $attribute_obj->is_taxonomy()) { $terms = wc_get_product_terms($product->get_id(), $name, array('fields' => 'all')); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => $term->slug, 'label' => $term->name, ); } } } else { $raw_options = array(); if (is_object($attribute_obj) && method_exists($attribute_obj, 'get_options')) { $raw_options = $attribute_obj->get_options(); } if (!empty($raw_options)) { foreach ($raw_options as $opt) { $clean_value = is_string($opt) ? trim($opt) : $opt; if ($clean_value === '' || $clean_value === null) continue; $options[] = array( 'value' => $clean_value, 'label' => mvx_get_readable_option_label($name, $clean_value), ); } } } if (!empty($options)) { $unique = array(); $final_options = array(); foreach ($options as $opt) { $key = (string) $opt['value']; if (isset($unique[$key])) continue; $unique[$key] = true; $final_options[] = $opt; } $result[] = array( 'name' => $name, 'label' => $label, 'options' => array_values($final_options), ); } } return $result; } /** * اطمینان از اینکه محصول variable باشد */ function mvx_ensure_variable_product($product_id) { $product = wc_get_product($product_id); if (!$product) return false; if ($product->is_type('variable')) { return true; } wp_set_object_terms($product_id, 'variable', 'product_type'); $product = wc_get_product($product_id); if (!$product) return false; return $product->is_type('variable'); } /** * افزودن attribute به محصول اگر وجود نداشت */ function mvx_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') { $product = wc_get_product($product_id); if (!$product) return false; $attributes = $product->get_attributes(); if (isset($attributes[$attribute_name])) { $attr_obj = $attributes[$attribute_name]; if (!mvx_is_taxonomy_attribute_name($attribute_name) && $attribute_value !== '') { $existing_options = method_exists($attr_obj, 'get_options') ? $attr_obj->get_options() : array(); if (!in_array($attribute_value, $existing_options, true)) { $existing_options[] = $attribute_value; if (method_exists($attr_obj, 'set_options')) { $attr_obj->set_options($existing_options); $attr_obj->set_variation(true); $attr_obj->set_visible(true); $attributes[$attribute_name] = $attr_obj; $product->set_attributes($attributes); $product->save(); } } } return true; } $new_attr = new WC_Product_Attribute(); if (mvx_is_taxonomy_attribute_name($attribute_name)) { $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0; $new_attr->set_id($taxonomy_id); $new_attr->set_name($attribute_name); $new_attr->set_options(array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } else { $new_attr->set_id(0); $new_attr->set_name($attribute_name); $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array()); $new_attr->set_position(count($attributes)); $new_attr->set_visible(true); $new_attr->set_variation(true); } $attributes[$attribute_name] = $new_attr; $product->set_attributes($attributes); $product->save(); return true; } /** * آیا variation تکراری است؟ */ function mvx_variation_exists($product_id, $new_attributes) { $children = get_posts(array( 'post_parent' => $product_id, 'post_type' => 'product_variation', 'numberposts' => -1, 'post_status' => array('publish', 'private'), 'fields' => 'ids', )); if (empty($children)) return false; foreach ($children as $variation_id) { $existing = get_post_meta($variation_id); $matched = true; foreach ($new_attributes as $meta_key => $meta_value) { $existing_value = isset($existing[$meta_key][0]) ? $existing[$meta_key][0] : ''; if ((string)$existing_value !== (string)$meta_value) { $matched = false; break; } } foreach ($existing as $meta_key => $meta_val) { if (strpos($meta_key, 'attribute_') === 0 && !array_key_exists($meta_key, $new_attributes)) { $extra_existing = isset($meta_val[0]) ? $meta_val[0] : ''; if ($extra_existing !== '') { $matched = false; break; } } } if ($matched) return true; } return false; } /** * فرم فرانت */ function mvx_render_quick_variation_form() { if (!is_product()) return; if (!mvx_is_admin_user()) return; global $product; if (!$product || !is_a($product, 'WC_Product')) return; $product_id = $product->get_id(); $attributes = mvx_get_all_selectable_attributes($product); if (empty($attributes)) { echo '<div style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fff8e5;border-radius:8px;">هیچ ویژگی سراسری یا محلی برای انتخاب پیدا نشد.</div>'; return; } ?> <div class="mvx-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;"> <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3> <form method="post" class="mvx-quick-variation-form" autocomplete="off" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <?php wp_nonce_field('mvx_quick_variation_action', 'mvx_quick_variation_nonce'); ?> <input type="hidden" name="mvx_product_id" value="<?php echo esc_attr($product_id); ?>"> <div> <label style="display:block;margin-bottom:6px;">ویژگی اول</label> <select name="mvx_attr1" id="mvx_qv_attr1" class="mvx-qv-select" style="width:100%;padding:8px;"> <option value="">انتخاب ویژگی</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label> <select name="mvx_val1" id="mvx_qv_val1" class="mvx-qv-select" data-placeholder="ابتدا ویژگی را انتخاب کنید" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div> <label style="display:block;margin-bottom:6px;">ویژگی دوم</label> <select name="mvx_attr2" id="mvx_qv_attr2" class="mvx-qv-select" style="width:100%;padding:8px;"> <option value="">بدون ویژگی دوم</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label> <select name="mvx_val2" id="mvx_qv_val2" class="mvx-qv-select" data-placeholder="ابتدا ویژگی را انتخاب کنید" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div style="grid-column:1 / -1;"> <label style="display:block;margin-bottom:6px;">قیمت</label> <input type="number" step="0.01" min="0" name="mvx_price" style="width:100%;padding:8px;" required> </div> <div style="grid-column:1 / -1;"> <button type="submit" name="mvx_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;"> افزودن تنوع </button> </div> </form> </div> <script> (function(){ var data = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>; var form = document.querySelector('.mvx-quick-variation-form'); if (!form) return; var attr1 = form.querySelector('#mvx_qv_attr1'); var val1 = form.querySelector('#mvx_qv_val1'); var attr2 = form.querySelector('#mvx_qv_attr2'); var val2 = form.querySelector('#mvx_qv_val2'); function getAttr(name) { for (var i = 0; i < data.length; i++) { if (data[i].name === name) return data[i]; } return null; } function refill(attrSelect, valSelect, keepValue) { var attrName = attrSelect.value; var oldValue = keepValue ? valSelect.value : ''; var placeholder = valSelect.getAttribute('data-placeholder') || 'ابتدا ویژگی را انتخاب کنید'; valSelect.innerHTML = ''; if (!attrName) { var emptyOp = document.createElement('option'); emptyOp.value = ''; emptyOp.textContent = placeholder; valSelect.appendChild(emptyOp); valSelect.value = ''; return; } var attr = getAttr(attrName); var items = (attr && attr.options) ? attr.options : []; var first = document.createElement('option'); first.value = ''; first.textContent = 'انتخاب مقدار'; valSelect.appendChild(first); var any = document.createElement('option'); any.value = '__any__'; any.textContent = 'همه موارد'; valSelect.appendChild(any); items.forEach(function(item) { var op = document.createElement('option'); op.value = item.value; op.textContent = item.label; valSelect.appendChild(op); }); if (oldValue) { var exists = Array.prototype.some.call(valSelect.options, function(option) { return option.value === oldValue; }); valSelect.value = exists ? oldValue : ''; } else { valSelect.value = ''; } } attr1.addEventListener('change', function() { refill(attr1, val1, false); if (attr2.value && attr2.value === attr1.value) { attr2.value = ''; refill(attr2, val2, false); } }); attr2.addEventListener('change', function() { if (attr1.value && attr2.value && attr1.value === attr2.value) { alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.'); attr2.value = ''; } refill(attr2, val2, false); }); [attr1, val1, attr2, val2].forEach(function(el){ el.addEventListener('change', function(e){ e.stopPropagation(); }, true); el.addEventListener('click', function(e){ e.stopPropagation(); }, true); }); })(); </script> <?php } /** * ثبت فرم */ function mvx_handle_quick_variation_submit() { if (!isset($_POST['mvx_quick_variation_submit'])) return; if (!mvx_is_admin_user()) return; if (!isset($_POST['mvx_quick_variation_nonce']) || !wp_verify_nonce($_POST['mvx_quick_variation_nonce'], 'mvx_quick_variation_action')) { wc_add_notice('خطای امنیتی رخ داد.', 'error'); return; } $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0; $attr1 = isset($_POST['mvx_attr1']) ? wc_clean(wp_unslash($_POST['mvx_attr1'])) : ''; $val1 = isset($_POST['mvx_val1']) ? wc_clean(wp_unslash($_POST['mvx_val1'])) : ''; $attr2 = isset($_POST['mvx_attr2']) ? wc_clean(wp_unslash($_POST['mvx_attr2'])) : ''; $val2 = isset($_POST['mvx_val2']) ? wc_clean(wp_unslash($_POST['mvx_val2'])) : ''; $price = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : ''; if (!$product_id || !$attr1 || $val1 === '' || $price === '') { wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error'); return; } if ($attr2 && !$val2 && $val2 !== '__any__') { wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error'); return; } if ($attr1 && $attr2 && $attr1 === $attr2) { wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error'); return; } if ($val1 === '__any__' && $val2 === '__any__') { wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان "همه موارد" انتخاب کرد.', 'error'); return; } if (!mvx_ensure_variable_product($product_id)) { wc_add_notice('تبدیل محصول به متغیر انجام نشد.', 'error'); return; } mvx_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : ''); if ($attr2) { mvx_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : ''); } $variation_attributes = array( 'attribute_' . sanitize_title($attr1) => ($val1 === '__any__' ? '' : $val1), ); if ($attr2) { $variation_attributes['attribute_' . sanitize_title($attr2)] = ($val2 === '__any__' ? '' : $val2); } if (mvx_variation_exists($product_id, $variation_attributes)) { wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error'); return; } $variation_post = array( 'post_title' => 'Product variation', 'post_name' => 'product-' . $product_id . '-variation', 'post_status' => 'publish', 'post_parent' => $product_id, 'post_type' => 'product_variation', 'guid' => home_url('/?product_variation=product-' . $product_id . '-variation'), ); $variation_id = wp_insert_post($variation_post); if (!$variation_id || is_wp_error($variation_id)) { wc_add_notice('ایجاد تنوع ناموفق بود.', 'error'); return; } foreach ($variation_attributes as $meta_key => $meta_value) { update_post_meta($variation_id, $meta_key, $meta_value); } update_post_meta($variation_id, '_regular_price', $price); update_post_meta($variation_id, '_price', $price); $variation = new WC_Product_Variation($variation_id); $variation->set_parent_id($product_id); $variation->set_regular_price($price); $variation->set_price($price); $variation->set_attributes(array( sanitize_title($attr1) => ($val1 === '__any__' ? '' : $val1), ) + ($attr2 ? array( sanitize_title($attr2) => ($val2 === '__any__' ? '' : $val2), ) : array())); $variation->save(); $product = wc_get_product($product_id); if ($product && is_a($product, 'WC_Product_Variable')) { WC_Product_Variable::sync($product_id); wc_delete_product_transients($product_id); } wc_add_notice('تنوع جدید با موفقیت ایجاد شد.', 'success'); wp_safe_redirect(get_permalink($product_id)); exit; } add_action('template_redirect', 'mvx_handle_quick_variation_submit'); /** * نمایش فرم */ add_action('woocommerce_after_single_product_summary', 'mvx_render_quick_variation_form', 5);
if (!defined('ABSPATH')) exit;

/**
 * فقط ادمین
 */
function mvx_is_admin_user() {
    return current_user_can('manage_woocommerce') || current_user_can('administrator');
}

/**
 * تشخیص اینکه ویژگی taxonomy/global است یا local
 */
function mvx_is_taxonomy_attribute_name($name) {
    return taxonomy_exists($name) && strpos($name, 'pa_') === 0;
}

/**
 * گرفتن لیبل ویژگی
 */
function mvx_get_attribute_label_safe($name, $product = null) {
    if (function_exists('wc_attribute_label')) {
        $label = wc_attribute_label($name, $product);
        if (!empty($label)) return $label;
    }

    if (strpos($name, 'pa_') === 0) {
        return ucfirst(str_replace(array('pa_', '-', '_'), array('', ' ', ' '), $name));
    }

    return ucfirst(str_replace(array('-', '_'), ' ', $name));
}

/**
 * تبدیل اسلاگ یا مقدار خام به متن قابل نمایش
 */
function mvx_get_readable_option_label($attribute_name, $option_value) {
    if ($option_value === '' || $option_value === null) {
        return '';
    }

    if (mvx_is_taxonomy_attribute_name($attribute_name)) {
        $term = get_term_by('slug', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }

        $term = get_term_by('name', $option_value, $attribute_name);
        if ($term && !is_wp_error($term)) {
            return $term->name;
        }
    }

    $decoded = rawurldecode($option_value);
    $decoded = html_entity_decode($decoded, ENT_QUOTES, 'UTF-8');
    return $decoded;
}

/**
 * گرفتن همه ویژگی‌های قابل انتخاب محصول
 * شامل global و local
 */
function mvx_get_all_selectable_attributes($product) {
    $result = array();
    $attributes = $product->get_attributes();

    if (empty($attributes)) {
        return $result;
    }

    foreach ($attributes as $attribute_name => $attribute_obj) {

        if (is_object($attribute_obj) && method_exists($attribute_obj, 'get_visible') && !$attribute_obj->get_visible()) {
            continue;
        }

        $name  = is_object($attribute_obj) && method_exists($attribute_obj, 'get_name') ? $attribute_obj->get_name() : $attribute_name;
        $label = mvx_get_attribute_label_safe($name, $product);

        $options = array();

        if (is_object($attribute_obj) && method_exists($attribute_obj, 'is_taxonomy') && $attribute_obj->is_taxonomy()) {
            $terms = wc_get_product_terms($product->get_id(), $name, array('fields' => 'all'));

            if (!empty($terms) && !is_wp_error($terms)) {
                foreach ($terms as $term) {
                    $options[] = array(
                        'value' => $term->slug,
                        'label' => $term->name,
                    );
                }
            }
        } else {
            $raw_options = array();

            if (is_object($attribute_obj) && method_exists($attribute_obj, 'get_options')) {
                $raw_options = $attribute_obj->get_options();
            }

            if (!empty($raw_options)) {
                foreach ($raw_options as $opt) {
                    $clean_value = is_string($opt) ? trim($opt) : $opt;
                    if ($clean_value === '' || $clean_value === null) continue;

                    $options[] = array(
                        'value' => $clean_value,
                        'label' => mvx_get_readable_option_label($name, $clean_value),
                    );
                }
            }
        }

        if (!empty($options)) {
            $unique = array();
            $final_options = array();

            foreach ($options as $opt) {
                $key = (string) $opt['value'];
                if (isset($unique[$key])) continue;
                $unique[$key] = true;
                $final_options[] = $opt;
            }

            $result[] = array(
                'name'    => $name,
                'label'   => $label,
                'options' => array_values($final_options),
            );
        }
    }

    return $result;
}

/**
 * اطمینان از اینکه محصول variable باشد
 */
function mvx_ensure_variable_product($product_id) {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    if ($product->is_type('variable')) {
        return true;
    }

    wp_set_object_terms($product_id, 'variable', 'product_type');

    $product = wc_get_product($product_id);
    if (!$product) return false;

    return $product->is_type('variable');
}

/**
 * افزودن attribute به محصول اگر وجود نداشت
 */
function mvx_attach_attribute_to_product_if_missing($product_id, $attribute_name, $attribute_value = '') {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    $attributes = $product->get_attributes();

    if (isset($attributes[$attribute_name])) {
        $attr_obj = $attributes[$attribute_name];

        if (!mvx_is_taxonomy_attribute_name($attribute_name) && $attribute_value !== '') {
            $existing_options = method_exists($attr_obj, 'get_options') ? $attr_obj->get_options() : array();
            if (!in_array($attribute_value, $existing_options, true)) {
                $existing_options[] = $attribute_value;
                if (method_exists($attr_obj, 'set_options')) {
                    $attr_obj->set_options($existing_options);
                    $attr_obj->set_variation(true);
                    $attr_obj->set_visible(true);
                    $attributes[$attribute_name] = $attr_obj;
                    $product->set_attributes($attributes);
                    $product->save();
                }
            }
        }

        return true;
    }

    $new_attr = new WC_Product_Attribute();

    if (mvx_is_taxonomy_attribute_name($attribute_name)) {
        $taxonomy_id = function_exists('wc_attribute_taxonomy_id_by_name') ? wc_attribute_taxonomy_id_by_name($attribute_name) : 0;
        $new_attr->set_id($taxonomy_id);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options(array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    } else {
        $new_attr->set_id(0);
        $new_attr->set_name($attribute_name);
        $new_attr->set_options($attribute_value !== '' ? array($attribute_value) : array());
        $new_attr->set_position(count($attributes));
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
    }

    $attributes[$attribute_name] = $new_attr;
    $product->set_attributes($attributes);
    $product->save();

    return true;
}

/**
 * آیا variation تکراری است؟
 */
function mvx_variation_exists($product_id, $new_attributes) {
    $children = get_posts(array(
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'numberposts' => -1,
        'post_status' => array('publish', 'private'),
        'fields'      => 'ids',
    ));

    if (empty($children)) return false;

    foreach ($children as $variation_id) {
        $existing = get_post_meta($variation_id);

        $matched = true;
        foreach ($new_attributes as $meta_key => $meta_value) {
            $existing_value = isset($existing[$meta_key][0]) ? $existing[$meta_key][0] : '';
            if ((string)$existing_value !== (string)$meta_value) {
                $matched = false;
                break;
            }
        }

        foreach ($existing as $meta_key => $meta_val) {
            if (strpos($meta_key, 'attribute_') === 0 && !array_key_exists($meta_key, $new_attributes)) {
                $extra_existing = isset($meta_val[0]) ? $meta_val[0] : '';
                if ($extra_existing !== '') {
                    $matched = false;
                    break;
                }
            }
        }

        if ($matched) return true;
    }

    return false;
}

/**
 * فرم فرانت
 */
function mvx_render_quick_variation_form() {
    if (!is_product()) return;
    if (!mvx_is_admin_user()) return;

    global $product;
    if (!$product || !is_a($product, 'WC_Product')) return;

    $product_id = $product->get_id();
    $attributes = mvx_get_all_selectable_attributes($product);

    if (empty($attributes)) {
        echo '<div style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fff8e5;border-radius:8px;">هیچ ویژگی سراسری یا محلی برای انتخاب پیدا نشد.</div>';
        return;
    }
    ?>
    <div class="mvx-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;">
        <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3>

        <form method="post" class="mvx-quick-variation-form" autocomplete="off" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
            <?php wp_nonce_field('mvx_quick_variation_action', 'mvx_quick_variation_nonce'); ?>

            <input type="hidden" name="mvx_product_id" value="<?php echo esc_attr($product_id); ?>">

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی اول</label>
                <select name="mvx_attr1" id="mvx_qv_attr1" class="mvx-qv-select" style="width:100%;padding:8px;">
                    <option value="">انتخاب ویژگی</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label>
                <select name="mvx_val1" id="mvx_qv_val1" class="mvx-qv-select" data-placeholder="ابتدا ویژگی را انتخاب کنید" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی دوم</label>
                <select name="mvx_attr2" id="mvx_qv_attr2" class="mvx-qv-select" style="width:100%;padding:8px;">
                    <option value="">بدون ویژگی دوم</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label>
                <select name="mvx_val2" id="mvx_qv_val2" class="mvx-qv-select" data-placeholder="ابتدا ویژگی را انتخاب کنید" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div style="grid-column:1 / -1;">
                <label style="display:block;margin-bottom:6px;">قیمت</label>
                <input type="number" step="0.01" min="0" name="mvx_price" style="width:100%;padding:8px;" required>
            </div>

            <div style="grid-column:1 / -1;">
                <button type="submit" name="mvx_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;">
                    افزودن تنوع
                </button>
            </div>
        </form>
    </div>

    <script>
    (function(){
        var data = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;

        var form  = document.querySelector('.mvx-quick-variation-form');
        if (!form) return;

        var attr1 = form.querySelector('#mvx_qv_attr1');
        var val1  = form.querySelector('#mvx_qv_val1');
        var attr2 = form.querySelector('#mvx_qv_attr2');
        var val2  = form.querySelector('#mvx_qv_val2');

        function getAttr(name) {
            for (var i = 0; i < data.length; i++) {
                if (data[i].name === name) return data[i];
            }
            return null;
        }

        function refill(attrSelect, valSelect, keepValue) {
            var attrName = attrSelect.value;
            var oldValue = keepValue ? valSelect.value : '';
            var placeholder = valSelect.getAttribute('data-placeholder') || 'ابتدا ویژگی را انتخاب کنید';

            valSelect.innerHTML = '';

            if (!attrName) {
                var emptyOp = document.createElement('option');
                emptyOp.value = '';
                emptyOp.textContent = placeholder;
                valSelect.appendChild(emptyOp);
                valSelect.value = '';
                return;
            }

            var attr = getAttr(attrName);
            var items = (attr && attr.options) ? attr.options : [];

            var first = document.createElement('option');
            first.value = '';
            first.textContent = 'انتخاب مقدار';
            valSelect.appendChild(first);

            var any = document.createElement('option');
            any.value = '__any__';
            any.textContent = 'همه موارد';
            valSelect.appendChild(any);

            items.forEach(function(item) {
                var op = document.createElement('option');
                op.value = item.value;
                op.textContent = item.label;
                valSelect.appendChild(op);
            });

            if (oldValue) {
                var exists = Array.prototype.some.call(valSelect.options, function(option) {
                    return option.value === oldValue;
                });
                valSelect.value = exists ? oldValue : '';
            } else {
                valSelect.value = '';
            }
        }

        attr1.addEventListener('change', function() {
            refill(attr1, val1, false);

            if (attr2.value && attr2.value === attr1.value) {
                attr2.value = '';
                refill(attr2, val2, false);
            }
        });

        attr2.addEventListener('change', function() {
            if (attr1.value && attr2.value && attr1.value === attr2.value) {
                alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.');
                attr2.value = '';
            }
            refill(attr2, val2, false);
        });

        [attr1, val1, attr2, val2].forEach(function(el){
            el.addEventListener('change', function(e){
                e.stopPropagation();
            }, true);

            el.addEventListener('click', function(e){
                e.stopPropagation();
            }, true);
        });
    })();
    </script>
    <?php
}

/**
 * ثبت فرم
 */
function mvx_handle_quick_variation_submit() {
    if (!isset($_POST['mvx_quick_variation_submit'])) return;
    if (!mvx_is_admin_user()) return;

    if (!isset($_POST['mvx_quick_variation_nonce']) || !wp_verify_nonce($_POST['mvx_quick_variation_nonce'], 'mvx_quick_variation_action')) {
        wc_add_notice('خطای امنیتی رخ داد.', 'error');
        return;
    }

    $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0;
    $attr1      = isset($_POST['mvx_attr1']) ? wc_clean(wp_unslash($_POST['mvx_attr1'])) : '';
    $val1       = isset($_POST['mvx_val1']) ? wc_clean(wp_unslash($_POST['mvx_val1'])) : '';
    $attr2      = isset($_POST['mvx_attr2']) ? wc_clean(wp_unslash($_POST['mvx_attr2'])) : '';
    $val2       = isset($_POST['mvx_val2']) ? wc_clean(wp_unslash($_POST['mvx_val2'])) : '';
    $price      = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : '';

    if (!$product_id || !$attr1 || $val1 === '' || $price === '') {
        wc_add_notice('لطفاً ویژگی اول، مقدار آن و قیمت را کامل وارد کنید.', 'error');
        return;
    }

    if ($attr2 && !$val2 && $val2 !== '__any__') {
        wc_add_notice('برای ویژگی دوم باید مقدار انتخاب کنید.', 'error');
        return;
    }

    if ($attr1 && $attr2 && $attr1 === $attr2) {
        wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error');
        return;
    }

    if ($val1 === '__any__' && $val2 === '__any__') {
        wc_add_notice('نمی‌توان برای هر دو ویژگی همزمان "همه موارد" انتخاب کرد.', 'error');
        return;
    }

    if (!mvx_ensure_variable_product($product_id)) {
        wc_add_notice('تبدیل محصول به متغیر انجام نشد.', 'error');
        return;
    }

    mvx_attach_attribute_to_product_if_missing($product_id, $attr1, $val1 !== '__any__' ? $val1 : '');
    if ($attr2) {
        mvx_attach_attribute_to_product_if_missing($product_id, $attr2, $val2 !== '__any__' ? $val2 : '');
    }

    $variation_attributes = array(
        'attribute_' . sanitize_title($attr1) => ($val1 === '__any__' ? '' : $val1),
    );

    if ($attr2) {
        $variation_attributes['attribute_' . sanitize_title($attr2)] = ($val2 === '__any__' ? '' : $val2);
    }

    if (mvx_variation_exists($product_id, $variation_attributes)) {
        wc_add_notice('این تنوع قبلاً ثبت شده است.', 'error');
        return;
    }

    $variation_post = array(
        'post_title'  => 'Product variation',
        'post_name'   => 'product-' . $product_id . '-variation',
        'post_status' => 'publish',
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'guid'        => home_url('/?product_variation=product-' . $product_id . '-variation'),
    );

    $variation_id = wp_insert_post($variation_post);

    if (!$variation_id || is_wp_error($variation_id)) {
        wc_add_notice('ایجاد تنوع ناموفق بود.', 'error');
        return;
    }

    foreach ($variation_attributes as $meta_key => $meta_value) {
        update_post_meta($variation_id, $meta_key, $meta_value);
    }

    update_post_meta($variation_id, '_regular_price', $price);
    update_post_meta($variation_id, '_price', $price);

    $variation = new WC_Product_Variation($variation_id);
    $variation->set_parent_id($product_id);
    $variation->set_regular_price($price);
    $variation->set_price($price);
    $variation->set_attributes(array(
        sanitize_title($attr1) => ($val1 === '__any__' ? '' : $val1),
    ) + ($attr2 ? array(
        sanitize_title($attr2) => ($val2 === '__any__' ? '' : $val2),
    ) : array()));
    $variation->save();

    $product = wc_get_product($product_id);
    if ($product && is_a($product, 'WC_Product_Variable')) {
        WC_Product_Variable::sync($product_id);
        wc_delete_product_transients($product_id);
    }

    wc_add_notice('تنوع جدید با موفقیت ایجاد شد.', 'success');

    wp_safe_redirect(get_permalink($product_id));
    exit;
}
add_action('template_redirect', 'mvx_handle_quick_variation_submit');

/**
 * نمایش فرم
 */
add_action('woocommerce_after_single_product_summary', 'mvx_render_quick_variation_form', 5);
تنوع ۱۹
TEXT - 2026-05-05 22:59:10
<?php if (!defined('ABSPATH')) exit; /** * ========================================================== * Quick Add WooCommerce Variation - Full Restored Version * نمایش همه ویژگی‌های سراسری + همه مقدارها * ========================================================== */ function mvx_normalize_text($value) { $value = is_string($value) ? wp_unslash($value) : $value; $value = wc_clean($value); return trim((string) $value); } function mvx_is_admin_user() { return current_user_can('manage_woocommerce') || current_user_can('administrator'); } /** * گرفتن همه ویژگی‌ها: * 1. همه ویژگی‌های سراسری ووکامرس + همه term ها * 2. ویژگی‌های محلی خود محصول */ function mvx_get_all_selectable_attributes($product = null) { $result = array(); $used_names = array(); /** * ویژگی‌های سراسری ووکامرس */ if (function_exists('wc_get_attribute_taxonomies')) { $taxonomies = wc_get_attribute_taxonomies(); if (!empty($taxonomies)) { foreach ($taxonomies as $tax) { if (empty($tax->attribute_name)) continue; $taxonomy = wc_attribute_taxonomy_name($tax->attribute_name); if (!taxonomy_exists($taxonomy)) continue; $label = !empty($tax->attribute_label) ? $tax->attribute_label : $tax->attribute_name; $terms = get_terms(array( 'taxonomy' => $taxonomy, 'hide_empty' => false, )); $options = array(); if (!is_wp_error($terms) && !empty($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => (string) $term->slug, 'label' => (string) $term->name, ); } } $result[] = array( 'name' => $taxonomy, 'label' => $label, 'is_taxonomy' => true, 'options' => $options, ); $used_names[$taxonomy] = true; } } } /** * ویژگی‌های محلی محصول */ if ($product && is_a($product, 'WC_Product')) { $attributes = $product->get_attributes(); if (!empty($attributes)) { foreach ($attributes as $key => $attribute) { if (!is_a($attribute, 'WC_Product_Attribute')) continue; $name = $attribute->get_name(); if (isset($used_names[$name])) continue; $label = wc_attribute_label($name); $options = array(); if ($attribute->is_taxonomy() && taxonomy_exists($name)) { $terms = wc_get_product_terms($product->get_id(), $name, array('fields' => 'all')); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => (string) $term->slug, 'label' => (string) $term->name, ); } } $result[] = array( 'name' => $name, 'label' => $label ? $label : $name, 'is_taxonomy' => true, 'options' => $options, ); } else { $raw_options = $attribute->get_options(); if (!empty($raw_options)) { foreach ($raw_options as $opt) { $opt = (string) $opt; $options[] = array( 'value' => $opt, 'label' => $opt, ); } } $result[] = array( 'name' => $name, 'label' => $label ? $label : $name, 'is_taxonomy' => false, 'options' => $options, ); } $used_names[$name] = true; } } } return $result; } function mvx_find_attribute_def_by_name($attr_name, $product = null) { $attributes = mvx_get_all_selectable_attributes($product); foreach ($attributes as $attr) { if ($attr['name'] === $attr_name) { return $attr; } } return false; } function mvx_get_term_from_posted_value($taxonomy, $posted_value) { if (!taxonomy_exists($taxonomy)) return false; $posted_value = mvx_normalize_text($posted_value); if ($posted_value === '') return false; $term = get_term_by('slug', $posted_value, $taxonomy); if ($term && !is_wp_error($term)) { return $term; } $term = get_term_by('name', $posted_value, $taxonomy); if ($term && !is_wp_error($term)) { return $term; } $terms = get_terms(array( 'taxonomy' => $taxonomy, 'hide_empty' => false, )); if (!is_wp_error($terms) && !empty($terms)) { foreach ($terms as $t) { if ((string) $t->name === (string) $posted_value) { return $t; } if ((string) $t->slug === (string) $posted_value) { return $t; } } } return false; } function mvx_prepare_variation_value($attr_def, $raw_value) { if ($raw_value === '__any__') { return array( 'meta_value' => '', 'variation_value' => '', 'display_value' => 'همه موارد', ); } if (!$attr_def) { return array( 'meta_value' => (string) $raw_value, 'variation_value' => (string) $raw_value, 'display_value' => (string) $raw_value, ); } if (!empty($attr_def['is_taxonomy'])) { $taxonomy = $attr_def['name']; $term = mvx_get_term_from_posted_value($taxonomy, $raw_value); if ($term) { return array( 'meta_value' => (string) $term->slug, 'variation_value' => (string) $term->slug, 'display_value' => (string) $term->name, ); } return array( 'meta_value' => (string) $raw_value, 'variation_value' => (string) $raw_value, 'display_value' => (string) $raw_value, ); } return array( 'meta_value' => (string) $raw_value, 'variation_value' => (string) $raw_value, 'display_value' => (string) $raw_value, ); } function mvx_make_product_variable_if_needed($product_id) { $product = wc_get_product($product_id); if (!$product) return false; if ($product->is_type('variable')) { return $product; } wp_set_object_terms($product_id, 'variable', 'product_type'); delete_transient('wc_product_children_' . $product_id); wc_delete_product_transients($product_id); return wc_get_product($product_id); } /** * اضافه کردن ویژگی و مقدار به خود محصول */ function mvx_add_option_to_product_attribute($product_id, $attr_name, $posted_value, $attr_def = false) { if ($posted_value === '__any__') { return true; } $product = wc_get_product($product_id); if (!$product) return false; $attributes = $product->get_attributes(); $found = false; foreach ($attributes as $key => $attribute) { if (!is_a($attribute, 'WC_Product_Attribute')) continue; if ($attribute->get_name() !== $attr_name) continue; $found = true; if ($attribute->is_taxonomy() && taxonomy_exists($attr_name)) { $term = mvx_get_term_from_posted_value($attr_name, $posted_value); $attribute->set_id(wc_attribute_taxonomy_id_by_name($attr_name)); $attribute->set_visible(true); $attribute->set_variation(true); if ($term) { wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true); $current_options = (array) $attribute->get_options(); $current_options = array_map('intval', $current_options); if (!in_array((int) $term->term_id, $current_options, true)) { $current_options[] = (int) $term->term_id; } $attribute->set_options($current_options); } $attributes[$key] = $attribute; } else { $attr_value = (string) $posted_value; $options = (array) $attribute->get_options(); if (!in_array($attr_value, $options, true)) { $options[] = $attr_value; } $attribute->set_options($options); $attribute->set_visible(true); $attribute->set_variation(true); $attributes[$key] = $attribute; } } if (!$found) { $new_attr = new WC_Product_Attribute(); $new_attr->set_name($attr_name); $new_attr->set_visible(true); $new_attr->set_variation(true); $new_attr->set_position(count($attributes)); if ($attr_def && !empty($attr_def['is_taxonomy']) && taxonomy_exists($attr_name)) { $term = mvx_get_term_from_posted_value($attr_name, $posted_value); $new_attr->set_id(wc_attribute_taxonomy_id_by_name($attr_name)); if ($term) { wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true); $new_attr->set_options(array((int) $term->term_id)); } else { $new_attr->set_options(array()); } } else { $new_attr->set_id(0); $new_attr->set_options(array((string) $posted_value)); } $attributes[$attr_name] = $new_attr; } $product->set_attributes($attributes); $product->save(); return true; } function mvx_get_variation_signature($attrs) { ksort($attrs); return md5(wp_json_encode($attrs, JSON_UNESCAPED_UNICODE)); } function mvx_variation_exists($product_id, $candidate_attrs) { $product = wc_get_product($product_id); if (!$product || !$product->is_type('variable')) { return false; } $children = $product->get_children(); if (empty($children)) { return false; } $candidate_signature = mvx_get_variation_signature($candidate_attrs); foreach ($children as $child_id) { $variation = wc_get_product($child_id); if (!$variation || !is_a($variation, 'WC_Product_Variation')) continue; $existing = $variation->get_attributes(); $normalized = array(); foreach ($existing as $k => $v) { $clean_key = str_replace('attribute_', '', $k); $normalized[$clean_key] = (string) $v; } if (mvx_get_variation_signature($normalized) === $candidate_signature) { return $child_id; } } return false; } function mvx_create_variation($product_id, $attrs_for_variation, $regular_price) { $existing_id = mvx_variation_exists($product_id, $attrs_for_variation); if ($existing_id) { return new WP_Error('variation_exists', 'این تنوع قبلاً وجود دارد.'); } $variation_post = array( 'post_title' => 'Product Variation', 'post_name' => 'product-' . $product_id . '-variation-' . time() . '-' . wp_rand(100, 999), 'post_status' => 'publish', 'post_parent' => $product_id, 'post_type' => 'product_variation', 'guid' => home_url('/?product_variation=product-' . $product_id), ); $variation_id = wp_insert_post($variation_post); if (is_wp_error($variation_id) || !$variation_id) { return new WP_Error('variation_create_failed', 'خطا در ساخت تنوع.'); } $variation = new WC_Product_Variation($variation_id); foreach ($attrs_for_variation as $taxonomy => $value) { update_post_meta($variation_id, 'attribute_' . $taxonomy, $value); } $variation->set_props(array( 'regular_price' => wc_format_decimal($regular_price), 'price' => wc_format_decimal($regular_price), 'status' => 'publish', )); $variation->save(); wc_delete_product_transients($product_id); return $variation_id; } /** * Decode برای جلوگیری از نمایش مقادیر encode شده */ add_filter('woocommerce_variation_option_name', function($term_name) { if (!is_string($term_name)) return $term_name; return rawurldecode(wp_specialchars_decode($term_name, ENT_QUOTES)); }, 999); add_filter('woocommerce_get_item_data', function($item_data, $cart_item) { if (empty($item_data) || !is_array($item_data)) return $item_data; foreach ($item_data as $index => $item) { if (!empty($item['value']) && is_string($item['value'])) { $item_data[$index]['value'] = rawurldecode(wp_specialchars_decode($item['value'], ENT_QUOTES)); } if (!empty($item['display']) && is_string($item['display'])) { $item_data[$index]['display'] = rawurldecode(wp_specialchars_decode($item['display'], ENT_QUOTES)); } } return $item_data; }, 999, 2); /** * ثبت فرم */ function mvx_handle_form_submit() { if (!isset($_POST['mvx_quick_variation_submit'])) return; if (!mvx_is_admin_user()) return; if ( !isset($_POST['mvx_quick_variation_nonce']) || !wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['mvx_quick_variation_nonce'])), 'mvx_quick_variation_action' ) ) { wc_add_notice('اعتبارسنجی ناموفق بود.', 'error'); return; } $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0; $attr1 = isset($_POST['mvx_attr1']) ? mvx_normalize_text($_POST['mvx_attr1']) : ''; $val1 = isset($_POST['mvx_val1']) ? mvx_normalize_text($_POST['mvx_val1']) : ''; $attr2 = isset($_POST['mvx_attr2']) ? mvx_normalize_text($_POST['mvx_attr2']) : ''; $val2 = isset($_POST['mvx_val2']) ? mvx_normalize_text($_POST['mvx_val2']) : ''; $price = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : ''; if (!$product_id || !$attr1 || $val1 === '') { wc_add_notice('لطفاً ویژگی اول و مقدار آن را انتخاب کنید.', 'error'); return; } if ($price === '') { wc_add_notice('لطفاً قیمت را وارد کنید.', 'error'); return; } if ($attr2 && $val2 === '') { wc_add_notice('برای ویژگی دوم باید مقدار انتخاب شود.', 'error'); return; } if ($attr1 && $attr2 && $attr1 === $attr2) { wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error'); return; } if ($val1 === '__any__' && $val2 === '__any__') { wc_add_notice('نمی‌توان برای هر دو ویژگی "همه موارد" انتخاب کرد.', 'error'); return; } $product = mvx_make_product_variable_if_needed($product_id); if (!$product) { wc_add_notice('محصول پیدا نشد.', 'error'); return; } $attr1_def = mvx_find_attribute_def_by_name($attr1, $product); $attr2_def = $attr2 ? mvx_find_attribute_def_by_name($attr2, $product) : false; if (!$attr1_def) { wc_add_notice('ویژگی اول پیدا نشد.', 'error'); return; } if ($attr2 && !$attr2_def) { wc_add_notice('ویژگی دوم پیدا نشد.', 'error'); return; } mvx_add_option_to_product_attribute($product_id, $attr1, $val1, $attr1_def); if ($attr2 && $val2 !== '') { mvx_add_option_to_product_attribute($product_id, $attr2, $val2, $attr2_def); } $prepared1 = mvx_prepare_variation_value($attr1_def, $val1); $variation_attrs = array( $attr1 => $prepared1['variation_value'], ); if ($attr2 && $val2 !== '') { $prepared2 = mvx_prepare_variation_value($attr2_def, $val2); $variation_attrs[$attr2] = $prepared2['variation_value']; } $created = mvx_create_variation($product_id, $variation_attrs, $price); if (is_wp_error($created)) { wc_add_notice($created->get_error_message(), 'error'); return; } $product = wc_get_product($product_id); if ($product && $product->is_type('variable')) { WC_Product_Variable::sync($product_id); wc_delete_product_transients($product_id); } wc_add_notice('تنوع با موفقیت ساخته شد.', 'success'); } add_action('template_redirect', 'mvx_handle_form_submit'); /** * نمایش فرم در صفحه محصول */ function mvx_render_quick_variation_form() { if (!is_product()) return; if (!mvx_is_admin_user()) return; global $product; if (!$product || !is_a($product, 'WC_Product')) return; $product_id = $product->get_id(); $attributes = mvx_get_all_selectable_attributes($product); if (empty($attributes)) { echo '<div style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fff8e5;border-radius:8px;">هیچ ویژگی سراسری یا محلی برای انتخاب پیدا نشد.</div>'; return; } ?> <div class="mvx-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;"> <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3> <form method="post" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <?php wp_nonce_field('mvx_quick_variation_action', 'mvx_quick_variation_nonce'); ?> <input type="hidden" name="mvx_product_id" value="<?php echo esc_attr($product_id); ?>"> <div> <label style="display:block;margin-bottom:6px;">ویژگی اول</label> <select name="mvx_attr1" id="mvx_attr1" style="width:100%;padding:8px;"> <option value="">انتخاب ویژگی</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label> <select name="mvx_val1" id="mvx_val1" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div> <label style="display:block;margin-bottom:6px;">ویژگی دوم</label> <select name="mvx_attr2" id="mvx_attr2" style="width:100%;padding:8px;"> <option value="">بدون ویژگی دوم</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label> <select name="mvx_val2" id="mvx_val2" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div style="grid-column:1 / -1;"> <label style="display:block;margin-bottom:6px;">قیمت</label> <input type="number" step="0.01" min="0" name="mvx_price" style="width:100%;padding:8px;" required> </div> <div style="grid-column:1 / -1;"> <button type="submit" name="mvx_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;"> افزودن تنوع </button> </div> </form> </div> <script> (function(){ var data = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>; var attr1 = document.getElementById('mvx_attr1'); var val1 = document.getElementById('mvx_val1'); var attr2 = document.getElementById('mvx_attr2'); var val2 = document.getElementById('mvx_val2'); function getAttr(name) { for (var i = 0; i < data.length; i++) { if (data[i].name === name) { return data[i]; } } return null; } function refill(attrSelect, valSelect, emptyLabel) { var attrName = attrSelect.value; valSelect.innerHTML = ''; if (!attrName) { var op = document.createElement('option'); op.value = ''; op.textContent = emptyLabel || 'ابتدا ویژگی را انتخاب کنید'; valSelect.appendChild(op); return; } var attr = getAttr(attrName); var items = attr && attr.options ? attr.options : []; var first = document.createElement('option'); first.value = ''; first.textContent = 'انتخاب مقدار'; valSelect.appendChild(first); var any = document.createElement('option'); any.value = '__any__'; any.textContent = 'همه موارد'; valSelect.appendChild(any); items.forEach(function(item) { var op = document.createElement('option'); op.value = item.value; op.textContent = item.label; valSelect.appendChild(op); }); } attr1.addEventListener('change', function() { refill(attr1, val1, 'ابتدا ویژگی را انتخاب کنید'); if (attr2.value && attr2.value === attr1.value) { attr2.value = ''; refill(attr2, val2, 'ابتدا ویژگی را انتخاب کنید'); } }); attr2.addEventListener('change', function() { if (attr1.value && attr2.value && attr1.value === attr2.value) { alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.'); attr2.value = ''; } refill(attr2, val2, 'ابتدا ویژگی را انتخاب کنید'); }); })(); </script> <?php } add_action('woocommerce_single_product_summary', 'mvx_render_quick_variation_form', 35);
<?php
if (!defined('ABSPATH')) exit;

/**
 * ==========================================================
 * Quick Add WooCommerce Variation - Full Restored Version
 * نمایش همه ویژگی‌های سراسری + همه مقدارها
 * ==========================================================
 */

function mvx_normalize_text($value) {
    $value = is_string($value) ? wp_unslash($value) : $value;
    $value = wc_clean($value);
    return trim((string) $value);
}

function mvx_is_admin_user() {
    return current_user_can('manage_woocommerce') || current_user_can('administrator');
}

/**
 * گرفتن همه ویژگی‌ها:
 * 1. همه ویژگی‌های سراسری ووکامرس + همه term ها
 * 2. ویژگی‌های محلی خود محصول
 */
function mvx_get_all_selectable_attributes($product = null) {
    $result = array();
    $used_names = array();

    /**
     * ویژگی‌های سراسری ووکامرس
     */
    if (function_exists('wc_get_attribute_taxonomies')) {
        $taxonomies = wc_get_attribute_taxonomies();

        if (!empty($taxonomies)) {
            foreach ($taxonomies as $tax) {
                if (empty($tax->attribute_name)) continue;

                $taxonomy = wc_attribute_taxonomy_name($tax->attribute_name);

                if (!taxonomy_exists($taxonomy)) continue;

                $label = !empty($tax->attribute_label) ? $tax->attribute_label : $tax->attribute_name;

                $terms = get_terms(array(
                    'taxonomy'   => $taxonomy,
                    'hide_empty' => false,
                ));

                $options = array();

                if (!is_wp_error($terms) && !empty($terms)) {
                    foreach ($terms as $term) {
                        $options[] = array(
                            'value' => (string) $term->slug,
                            'label' => (string) $term->name,
                        );
                    }
                }

                $result[] = array(
                    'name'        => $taxonomy,
                    'label'       => $label,
                    'is_taxonomy' => true,
                    'options'     => $options,
                );

                $used_names[$taxonomy] = true;
            }
        }
    }

    /**
     * ویژگی‌های محلی محصول
     */
    if ($product && is_a($product, 'WC_Product')) {
        $attributes = $product->get_attributes();

        if (!empty($attributes)) {
            foreach ($attributes as $key => $attribute) {
                if (!is_a($attribute, 'WC_Product_Attribute')) continue;

                $name = $attribute->get_name();

                if (isset($used_names[$name])) continue;

                $label = wc_attribute_label($name);
                $options = array();

                if ($attribute->is_taxonomy() && taxonomy_exists($name)) {
                    $terms = wc_get_product_terms($product->get_id(), $name, array('fields' => 'all'));

                    if (!empty($terms) && !is_wp_error($terms)) {
                        foreach ($terms as $term) {
                            $options[] = array(
                                'value' => (string) $term->slug,
                                'label' => (string) $term->name,
                            );
                        }
                    }

                    $result[] = array(
                        'name'        => $name,
                        'label'       => $label ? $label : $name,
                        'is_taxonomy' => true,
                        'options'     => $options,
                    );

                } else {
                    $raw_options = $attribute->get_options();

                    if (!empty($raw_options)) {
                        foreach ($raw_options as $opt) {
                            $opt = (string) $opt;

                            $options[] = array(
                                'value' => $opt,
                                'label' => $opt,
                            );
                        }
                    }

                    $result[] = array(
                        'name'        => $name,
                        'label'       => $label ? $label : $name,
                        'is_taxonomy' => false,
                        'options'     => $options,
                    );
                }

                $used_names[$name] = true;
            }
        }
    }

    return $result;
}

function mvx_find_attribute_def_by_name($attr_name, $product = null) {
    $attributes = mvx_get_all_selectable_attributes($product);

    foreach ($attributes as $attr) {
        if ($attr['name'] === $attr_name) {
            return $attr;
        }
    }

    return false;
}

function mvx_get_term_from_posted_value($taxonomy, $posted_value) {
    if (!taxonomy_exists($taxonomy)) return false;

    $posted_value = mvx_normalize_text($posted_value);
    if ($posted_value === '') return false;

    $term = get_term_by('slug', $posted_value, $taxonomy);
    if ($term && !is_wp_error($term)) {
        return $term;
    }

    $term = get_term_by('name', $posted_value, $taxonomy);
    if ($term && !is_wp_error($term)) {
        return $term;
    }

    $terms = get_terms(array(
        'taxonomy'   => $taxonomy,
        'hide_empty' => false,
    ));

    if (!is_wp_error($terms) && !empty($terms)) {
        foreach ($terms as $t) {
            if ((string) $t->name === (string) $posted_value) {
                return $t;
            }

            if ((string) $t->slug === (string) $posted_value) {
                return $t;
            }
        }
    }

    return false;
}

function mvx_prepare_variation_value($attr_def, $raw_value) {
    if ($raw_value === '__any__') {
        return array(
            'meta_value'      => '',
            'variation_value' => '',
            'display_value'   => 'همه موارد',
        );
    }

    if (!$attr_def) {
        return array(
            'meta_value'      => (string) $raw_value,
            'variation_value' => (string) $raw_value,
            'display_value'   => (string) $raw_value,
        );
    }

    if (!empty($attr_def['is_taxonomy'])) {
        $taxonomy = $attr_def['name'];
        $term = mvx_get_term_from_posted_value($taxonomy, $raw_value);

        if ($term) {
            return array(
                'meta_value'      => (string) $term->slug,
                'variation_value' => (string) $term->slug,
                'display_value'   => (string) $term->name,
            );
        }

        return array(
            'meta_value'      => (string) $raw_value,
            'variation_value' => (string) $raw_value,
            'display_value'   => (string) $raw_value,
        );
    }

    return array(
        'meta_value'      => (string) $raw_value,
        'variation_value' => (string) $raw_value,
        'display_value'   => (string) $raw_value,
    );
}

function mvx_make_product_variable_if_needed($product_id) {
    $product = wc_get_product($product_id);

    if (!$product) return false;

    if ($product->is_type('variable')) {
        return $product;
    }

    wp_set_object_terms($product_id, 'variable', 'product_type');

    delete_transient('wc_product_children_' . $product_id);
    wc_delete_product_transients($product_id);

    return wc_get_product($product_id);
}

/**
 * اضافه کردن ویژگی و مقدار به خود محصول
 */
function mvx_add_option_to_product_attribute($product_id, $attr_name, $posted_value, $attr_def = false) {
    if ($posted_value === '__any__') {
        return true;
    }

    $product = wc_get_product($product_id);
    if (!$product) return false;

    $attributes = $product->get_attributes();
    $found = false;

    foreach ($attributes as $key => $attribute) {
        if (!is_a($attribute, 'WC_Product_Attribute')) continue;

        if ($attribute->get_name() !== $attr_name) continue;

        $found = true;

        if ($attribute->is_taxonomy() && taxonomy_exists($attr_name)) {
            $term = mvx_get_term_from_posted_value($attr_name, $posted_value);

            $attribute->set_id(wc_attribute_taxonomy_id_by_name($attr_name));
            $attribute->set_visible(true);
            $attribute->set_variation(true);

            if ($term) {
                wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true);

                $current_options = (array) $attribute->get_options();
                $current_options = array_map('intval', $current_options);

                if (!in_array((int) $term->term_id, $current_options, true)) {
                    $current_options[] = (int) $term->term_id;
                }

                $attribute->set_options($current_options);
            }

            $attributes[$key] = $attribute;

        } else {
            $attr_value = (string) $posted_value;
            $options = (array) $attribute->get_options();

            if (!in_array($attr_value, $options, true)) {
                $options[] = $attr_value;
            }

            $attribute->set_options($options);
            $attribute->set_visible(true);
            $attribute->set_variation(true);

            $attributes[$key] = $attribute;
        }
    }

    if (!$found) {
        $new_attr = new WC_Product_Attribute();
        $new_attr->set_name($attr_name);
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
        $new_attr->set_position(count($attributes));

        if ($attr_def && !empty($attr_def['is_taxonomy']) && taxonomy_exists($attr_name)) {
            $term = mvx_get_term_from_posted_value($attr_name, $posted_value);

            $new_attr->set_id(wc_attribute_taxonomy_id_by_name($attr_name));

            if ($term) {
                wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true);
                $new_attr->set_options(array((int) $term->term_id));
            } else {
                $new_attr->set_options(array());
            }

        } else {
            $new_attr->set_id(0);
            $new_attr->set_options(array((string) $posted_value));
        }

        $attributes[$attr_name] = $new_attr;
    }

    $product->set_attributes($attributes);
    $product->save();

    return true;
}

function mvx_get_variation_signature($attrs) {
    ksort($attrs);
    return md5(wp_json_encode($attrs, JSON_UNESCAPED_UNICODE));
}

function mvx_variation_exists($product_id, $candidate_attrs) {
    $product = wc_get_product($product_id);

    if (!$product || !$product->is_type('variable')) {
        return false;
    }

    $children = $product->get_children();

    if (empty($children)) {
        return false;
    }

    $candidate_signature = mvx_get_variation_signature($candidate_attrs);

    foreach ($children as $child_id) {
        $variation = wc_get_product($child_id);

        if (!$variation || !is_a($variation, 'WC_Product_Variation')) continue;

        $existing = $variation->get_attributes();
        $normalized = array();

        foreach ($existing as $k => $v) {
            $clean_key = str_replace('attribute_', '', $k);
            $normalized[$clean_key] = (string) $v;
        }

        if (mvx_get_variation_signature($normalized) === $candidate_signature) {
            return $child_id;
        }
    }

    return false;
}

function mvx_create_variation($product_id, $attrs_for_variation, $regular_price) {
    $existing_id = mvx_variation_exists($product_id, $attrs_for_variation);

    if ($existing_id) {
        return new WP_Error('variation_exists', 'این تنوع قبلاً وجود دارد.');
    }

    $variation_post = array(
        'post_title'  => 'Product Variation',
        'post_name'   => 'product-' . $product_id . '-variation-' . time() . '-' . wp_rand(100, 999),
        'post_status' => 'publish',
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'guid'        => home_url('/?product_variation=product-' . $product_id),
    );

    $variation_id = wp_insert_post($variation_post);

    if (is_wp_error($variation_id) || !$variation_id) {
        return new WP_Error('variation_create_failed', 'خطا در ساخت تنوع.');
    }

    $variation = new WC_Product_Variation($variation_id);

    foreach ($attrs_for_variation as $taxonomy => $value) {
        update_post_meta($variation_id, 'attribute_' . $taxonomy, $value);
    }

    $variation->set_props(array(
        'regular_price' => wc_format_decimal($regular_price),
        'price'         => wc_format_decimal($regular_price),
        'status'        => 'publish',
    ));

    $variation->save();

    wc_delete_product_transients($product_id);

    return $variation_id;
}

/**
 * Decode برای جلوگیری از نمایش مقادیر encode شده
 */
add_filter('woocommerce_variation_option_name', function($term_name) {
    if (!is_string($term_name)) return $term_name;

    return rawurldecode(wp_specialchars_decode($term_name, ENT_QUOTES));
}, 999);

add_filter('woocommerce_get_item_data', function($item_data, $cart_item) {
    if (empty($item_data) || !is_array($item_data)) return $item_data;

    foreach ($item_data as $index => $item) {
        if (!empty($item['value']) && is_string($item['value'])) {
            $item_data[$index]['value'] = rawurldecode(wp_specialchars_decode($item['value'], ENT_QUOTES));
        }

        if (!empty($item['display']) && is_string($item['display'])) {
            $item_data[$index]['display'] = rawurldecode(wp_specialchars_decode($item['display'], ENT_QUOTES));
        }
    }

    return $item_data;
}, 999, 2);

/**
 * ثبت فرم
 */
function mvx_handle_form_submit() {
    if (!isset($_POST['mvx_quick_variation_submit'])) return;
    if (!mvx_is_admin_user()) return;

    if (
        !isset($_POST['mvx_quick_variation_nonce']) ||
        !wp_verify_nonce(
            sanitize_text_field(wp_unslash($_POST['mvx_quick_variation_nonce'])),
            'mvx_quick_variation_action'
        )
    ) {
        wc_add_notice('اعتبارسنجی ناموفق بود.', 'error');
        return;
    }

    $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0;
    $attr1      = isset($_POST['mvx_attr1']) ? mvx_normalize_text($_POST['mvx_attr1']) : '';
    $val1       = isset($_POST['mvx_val1']) ? mvx_normalize_text($_POST['mvx_val1']) : '';
    $attr2      = isset($_POST['mvx_attr2']) ? mvx_normalize_text($_POST['mvx_attr2']) : '';
    $val2       = isset($_POST['mvx_val2']) ? mvx_normalize_text($_POST['mvx_val2']) : '';
    $price      = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : '';

    if (!$product_id || !$attr1 || $val1 === '') {
        wc_add_notice('لطفاً ویژگی اول و مقدار آن را انتخاب کنید.', 'error');
        return;
    }

    if ($price === '') {
        wc_add_notice('لطفاً قیمت را وارد کنید.', 'error');
        return;
    }

    if ($attr2 && $val2 === '') {
        wc_add_notice('برای ویژگی دوم باید مقدار انتخاب شود.', 'error');
        return;
    }

    if ($attr1 && $attr2 && $attr1 === $attr2) {
        wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error');
        return;
    }

    if ($val1 === '__any__' && $val2 === '__any__') {
        wc_add_notice('نمی‌توان برای هر دو ویژگی "همه موارد" انتخاب کرد.', 'error');
        return;
    }

    $product = mvx_make_product_variable_if_needed($product_id);

    if (!$product) {
        wc_add_notice('محصول پیدا نشد.', 'error');
        return;
    }

    $attr1_def = mvx_find_attribute_def_by_name($attr1, $product);
    $attr2_def = $attr2 ? mvx_find_attribute_def_by_name($attr2, $product) : false;

    if (!$attr1_def) {
        wc_add_notice('ویژگی اول پیدا نشد.', 'error');
        return;
    }

    if ($attr2 && !$attr2_def) {
        wc_add_notice('ویژگی دوم پیدا نشد.', 'error');
        return;
    }

    mvx_add_option_to_product_attribute($product_id, $attr1, $val1, $attr1_def);

    if ($attr2 && $val2 !== '') {
        mvx_add_option_to_product_attribute($product_id, $attr2, $val2, $attr2_def);
    }

    $prepared1 = mvx_prepare_variation_value($attr1_def, $val1);

    $variation_attrs = array(
        $attr1 => $prepared1['variation_value'],
    );

    if ($attr2 && $val2 !== '') {
        $prepared2 = mvx_prepare_variation_value($attr2_def, $val2);
        $variation_attrs[$attr2] = $prepared2['variation_value'];
    }

    $created = mvx_create_variation($product_id, $variation_attrs, $price);

    if (is_wp_error($created)) {
        wc_add_notice($created->get_error_message(), 'error');
        return;
    }

    $product = wc_get_product($product_id);

    if ($product && $product->is_type('variable')) {
        WC_Product_Variable::sync($product_id);
        wc_delete_product_transients($product_id);
    }

    wc_add_notice('تنوع با موفقیت ساخته شد.', 'success');
}
add_action('template_redirect', 'mvx_handle_form_submit');

/**
 * نمایش فرم در صفحه محصول
 */
function mvx_render_quick_variation_form() {
    if (!is_product()) return;
    if (!mvx_is_admin_user()) return;

    global $product;

    if (!$product || !is_a($product, 'WC_Product')) return;

    $product_id = $product->get_id();
    $attributes = mvx_get_all_selectable_attributes($product);

    if (empty($attributes)) {
        echo '<div style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fff8e5;border-radius:8px;">هیچ ویژگی سراسری یا محلی برای انتخاب پیدا نشد.</div>';
        return;
    }
    ?>

    <div class="mvx-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;">
        <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3>

        <form method="post" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
            <?php wp_nonce_field('mvx_quick_variation_action', 'mvx_quick_variation_nonce'); ?>

            <input type="hidden" name="mvx_product_id" value="<?php echo esc_attr($product_id); ?>">

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی اول</label>
                <select name="mvx_attr1" id="mvx_attr1" style="width:100%;padding:8px;">
                    <option value="">انتخاب ویژگی</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label>
                <select name="mvx_val1" id="mvx_val1" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی دوم</label>
                <select name="mvx_attr2" id="mvx_attr2" style="width:100%;padding:8px;">
                    <option value="">بدون ویژگی دوم</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label>
                <select name="mvx_val2" id="mvx_val2" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div style="grid-column:1 / -1;">
                <label style="display:block;margin-bottom:6px;">قیمت</label>
                <input type="number" step="0.01" min="0" name="mvx_price" style="width:100%;padding:8px;" required>
            </div>

            <div style="grid-column:1 / -1;">
                <button type="submit" name="mvx_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;">
                    افزودن تنوع
                </button>
            </div>
        </form>
    </div>

    <script>
    (function(){
        var data = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;

        var attr1 = document.getElementById('mvx_attr1');
        var val1  = document.getElementById('mvx_val1');
        var attr2 = document.getElementById('mvx_attr2');
        var val2  = document.getElementById('mvx_val2');

        function getAttr(name) {
            for (var i = 0; i < data.length; i++) {
                if (data[i].name === name) {
                    return data[i];
                }
            }

            return null;
        }

        function refill(attrSelect, valSelect, emptyLabel) {
            var attrName = attrSelect.value;

            valSelect.innerHTML = '';

            if (!attrName) {
                var op = document.createElement('option');
                op.value = '';
                op.textContent = emptyLabel || 'ابتدا ویژگی را انتخاب کنید';
                valSelect.appendChild(op);
                return;
            }

            var attr = getAttr(attrName);
            var items = attr && attr.options ? attr.options : [];

            var first = document.createElement('option');
            first.value = '';
            first.textContent = 'انتخاب مقدار';
            valSelect.appendChild(first);

            var any = document.createElement('option');
            any.value = '__any__';
            any.textContent = 'همه موارد';
            valSelect.appendChild(any);

            items.forEach(function(item) {
                var op = document.createElement('option');
                op.value = item.value;
                op.textContent = item.label;
                valSelect.appendChild(op);
            });
        }

        attr1.addEventListener('change', function() {
            refill(attr1, val1, 'ابتدا ویژگی را انتخاب کنید');

            if (attr2.value && attr2.value === attr1.value) {
                attr2.value = '';
                refill(attr2, val2, 'ابتدا ویژگی را انتخاب کنید');
            }
        });

        attr2.addEventListener('change', function() {
            if (attr1.value && attr2.value && attr1.value === attr2.value) {
                alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.');
                attr2.value = '';
            }

            refill(attr2, val2, 'ابتدا ویژگی را انتخاب کنید');
        });
    })();
    </script>

    <?php
}
add_action('woocommerce_single_product_summary', 'mvx_render_quick_variation_form', 35);
تنوع ۱۸
TEXT - 2026-05-05 22:46:24
<?php if (!defined('ABSPATH')) exit; /** * ============================================ * Quick Add WooCommerce Variation (Final) * - Admin only on single product page * - Supports global/local attributes * - Supports Persian values * - Prevents duplicate variations * - Supports "Any / همه موارد" * ============================================ */ /** * ---------- Helpers ---------- */ function mvx_normalize_text($value) { $value = is_string($value) ? wp_unslash($value) : $value; $value = wc_clean($value); return trim((string) $value); } function mvx_is_admin_user() { return current_user_can('manage_woocommerce') || current_user_can('administrator'); } function mvx_get_product_all_attributes($product) { if (!$product || !is_a($product, 'WC_Product')) return array(); $result = array(); $attributes = $product->get_attributes(); foreach ($attributes as $key => $attribute) { if (!is_a($attribute, 'WC_Product_Attribute')) continue; $name = $attribute->get_name(); $label = wc_attribute_label($name); $is_taxonomy = $attribute->is_taxonomy(); $options = array(); if ($is_taxonomy && taxonomy_exists($name)) { $terms = wc_get_product_terms($product->get_id(), $name, array('fields' => 'all')); if (!empty($terms) && !is_wp_error($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => (string) $term->slug, 'label' => (string) $term->name, ); } } } else { $raw_options = $attribute->get_options(); if (!empty($raw_options)) { foreach ($raw_options as $opt) { $opt = (string) $opt; $options[] = array( 'value' => $opt, 'label' => $opt, ); } } } $result[] = array( 'name' => $name, 'label' => $label ? $label : $name, 'is_taxonomy' => $is_taxonomy, 'options' => $options, ); } return $result; } function mvx_find_attribute_def($product, $attr_name) { $all = mvx_get_product_all_attributes($product); foreach ($all as $attr) { if ($attr['name'] === $attr_name) { return $attr; } } return false; } function mvx_get_term_from_posted_value($taxonomy, $posted_value) { if (!taxonomy_exists($taxonomy)) return false; $posted_value = mvx_normalize_text($posted_value); if ($posted_value === '') return false; $term = get_term_by('slug', $posted_value, $taxonomy); if ($term && !is_wp_error($term)) return $term; $terms = get_terms(array( 'taxonomy' => $taxonomy, 'hide_empty' => false, )); if (!is_wp_error($terms) && !empty($terms)) { foreach ($terms as $t) { if ((string) $t->name === (string) $posted_value) { return $t; } } } return false; } function mvx_prepare_variation_value($attr_def, $raw_value) { if ($raw_value === '__any__') { return array( 'meta_value' => '', 'variation_value' => '', 'display_value' => 'همه موارد', ); } if (!$attr_def) { return array( 'meta_value' => $raw_value, 'variation_value' => $raw_value, 'display_value' => $raw_value, ); } if (!empty($attr_def['is_taxonomy'])) { $taxonomy = $attr_def['name']; $term = mvx_get_term_from_posted_value($taxonomy, $raw_value); if ($term) { return array( 'meta_value' => $term->slug, 'variation_value' => $term->slug, 'display_value' => $term->name, ); } return array( 'meta_value' => '', 'variation_value' => '', 'display_value' => '', ); } return array( 'meta_value' => (string) $raw_value, 'variation_value' => (string) $raw_value, 'display_value' => (string) $raw_value, ); } function mvx_add_option_to_product_attribute($product_id, $attr_name, $posted_value, $attr_def = false) { if ($posted_value === '__any__') { return true; } $product = wc_get_product($product_id); if (!$product) return false; $attributes = $product->get_attributes(); $found = false; foreach ($attributes as $key => $attribute) { if (!is_a($attribute, 'WC_Product_Attribute')) continue; if ($attribute->get_name() !== $attr_name) continue; $found = true; if ($attribute->is_taxonomy()) { $term = false; if ($attr_def && !empty($attr_def['is_taxonomy'])) { $term = mvx_get_term_from_posted_value($attr_name, $posted_value); } if ($term) { wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true); $current_options = (array) $attribute->get_options(); $current_options = array_map('intval', $current_options); $term_id = (int) $term->term_id; if (!in_array($term_id, $current_options, true)) { $current_options[] = $term_id; } $attribute->set_options($current_options); } $attribute->set_id(wc_attribute_taxonomy_id_by_name($attr_name)); $attribute->set_visible(true); $attribute->set_variation(true); $attributes[$key] = $attribute; } else { $attr_value = (string) $posted_value; $options = (array) $attribute->get_options(); if (!in_array($attr_value, $options, true)) { $options[] = $attr_value; $attribute->set_options($options); } $attribute->set_visible(true); $attribute->set_variation(true); $attributes[$key] = $attribute; } } if (!$found) { $new_attr = new WC_Product_Attribute(); $new_attr->set_name($attr_name); $new_attr->set_visible(true); $new_attr->set_variation(true); $new_attr->set_position(count($attributes)); if ($attr_def && !empty($attr_def['is_taxonomy']) && taxonomy_exists($attr_name)) { $term = mvx_get_term_from_posted_value($attr_name, $posted_value); $new_attr->set_id(wc_attribute_taxonomy_id_by_name($attr_name)); if ($term) { wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true); $new_attr->set_options(array((int) $term->term_id)); } else { $new_attr->set_options(array()); } } else { $attr_value = (string) $posted_value; $new_attr->set_options(array($attr_value)); } $attributes[$attr_name] = $new_attr; } $product->set_attributes($attributes); $product->save(); return true; } function mvx_make_product_variable_if_needed($product_id) { $product = wc_get_product($product_id); if (!$product) return false; if ($product->is_type('variable')) { return $product; } wp_set_object_terms($product_id, 'variable', 'product_type'); delete_transient('wc_product_children_' . $product_id); wc_delete_product_transients($product_id); return wc_get_product($product_id); } function mvx_get_variation_signature($attrs) { ksort($attrs); return md5(wp_json_encode($attrs, JSON_UNESCAPED_UNICODE)); } function mvx_variation_exists($product_id, $candidate_attrs) { $product = wc_get_product($product_id); if (!$product || !$product->is_type('variable')) return false; $children = $product->get_children(); if (empty($children)) return false; $candidate_signature = mvx_get_variation_signature($candidate_attrs); foreach ($children as $child_id) { $variation = wc_get_product($child_id); if (!$variation || !is_a($variation, 'WC_Product_Variation')) continue; $existing = $variation->get_attributes(); $normalized = array(); foreach ($existing as $k => $v) { $tax_key = strpos($k, 'attribute_') === 0 ? $k : 'attribute_' . $k; $clean_key = str_replace('attribute_', '', $tax_key); $normalized[$clean_key] = (string) $v; } if (mvx_get_variation_signature($normalized) === $candidate_signature) { return $child_id; } } return false; } function mvx_create_variation($product_id, $attrs_for_variation, $regular_price) { $existing_id = mvx_variation_exists($product_id, $attrs_for_variation); if ($existing_id) { return new WP_Error('variation_exists', 'این تنوع قبلاً وجود دارد.'); } $variation_post = array( 'post_title' => 'Product Variation', 'post_name' => 'product-' . $product_id . '-variation-' . time() . '-' . wp_rand(100, 999), 'post_status' => 'publish', 'post_parent' => $product_id, 'post_type' => 'product_variation', 'guid' => home_url('/?product_variation=product-' . $product_id), ); $variation_id = wp_insert_post($variation_post); if (is_wp_error($variation_id) || !$variation_id) { return new WP_Error('variation_create_failed', 'خطا در ساخت تنوع.'); } $variation = new WC_Product_Variation($variation_id); foreach ($attrs_for_variation as $taxonomy => $value) { update_post_meta($variation_id, 'attribute_' . $taxonomy, $value); } $variation->set_props(array( 'regular_price' => wc_format_decimal($regular_price), 'price' => wc_format_decimal($regular_price), )); $variation->save(); wc_delete_product_transients($product_id); return $variation_id; } /** * جلوگیری از نمایش عجیب مقادیر variation در فرانت */ add_filter('woocommerce_variation_option_name', function($term_name) { if (!is_string($term_name)) return $term_name; $decoded = rawurldecode($term_name); $decoded = wp_specialchars_decode($decoded, ENT_QUOTES); return $decoded; }, 999); add_filter('woocommerce_product_variation_title_include_attributes', '__return_false', 999); add_filter('woocommerce_available_variation', function($data, $product, $variation) { if (!empty($data['attributes']) && is_array($data['attributes'])) { foreach ($data['attributes'] as $k => $v) { if (is_string($v)) { $data['attributes'][$k] = rawurldecode($v); } } } if (!empty($data['variation_description']) && is_string($data['variation_description'])) { $data['variation_description'] = rawurldecode($data['variation_description']); } return $data; }, 999, 3); /** * ---------- Form Submit ---------- */ function mvx_handle_form_submit() { if (!isset($_POST['mvx_quick_variation_submit'])) return; if (!mvx_is_admin_user()) return; if (!isset($_POST['mvx_quick_variation_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['mvx_quick_variation_nonce'])), 'mvx_quick_variation_action')) { wc_add_notice('اعتبارسنجی ناموفق بود.', 'error'); return; } $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0; $attr1 = isset($_POST['mvx_attr1']) ? mvx_normalize_text($_POST['mvx_attr1']) : ''; $val1 = isset($_POST['mvx_val1']) ? mvx_normalize_text($_POST['mvx_val1']) : ''; $attr2 = isset($_POST['mvx_attr2']) ? mvx_normalize_text($_POST['mvx_attr2']) : ''; $val2 = isset($_POST['mvx_val2']) ? mvx_normalize_text($_POST['mvx_val2']) : ''; $price = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : ''; if (!$product_id || !$attr1 || $val1 === '') { wc_add_notice('لطفاً ویژگی اول و مقدار آن را انتخاب کنید.', 'error'); return; } if ($price === '') { wc_add_notice('لطفاً قیمت را وارد کنید.', 'error'); return; } if ($attr2 && !$val2 && $val2 !== '0') { wc_add_notice('برای ویژگی دوم باید مقدار انتخاب شود.', 'error'); return; } if ($attr1 && $attr2 && $attr1 === $attr2) { wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error'); return; } if ($val1 === '__any__' && $val2 === '__any__') { wc_add_notice('نمی‌توان برای هر دو ویژگی "همه موارد" انتخاب کرد.', 'error'); return; } $product = mvx_make_product_variable_if_needed($product_id); if (!$product) { wc_add_notice('محصول پیدا نشد.', 'error'); return; } $attr1_def = mvx_find_attribute_def($product, $attr1); $attr2_def = $attr2 ? mvx_find_attribute_def($product, $attr2) : false; if (!$attr1_def) { wc_add_notice('ویژگی اول در محصول یافت نشد.', 'error'); return; } if ($attr2 && !$attr2_def) { wc_add_notice('ویژگی دوم در محصول یافت نشد.', 'error'); return; } mvx_add_option_to_product_attribute($product_id, $attr1, $val1, $attr1_def); if ($attr2 && $val2 !== '') { mvx_add_option_to_product_attribute($product_id, $attr2, $val2, $attr2_def); } $prepared1 = mvx_prepare_variation_value($attr1_def, $val1); $variation_attrs = array( $attr1 => $prepared1['variation_value'], ); if ($attr2 && $val2 !== '') { $prepared2 = mvx_prepare_variation_value($attr2_def, $val2); $variation_attrs[$attr2] = $prepared2['variation_value']; } $created = mvx_create_variation($product_id, $variation_attrs, $price); if (is_wp_error($created)) { wc_add_notice($created->get_error_message(), 'error'); return; } $product = wc_get_product($product_id); if ($product && $product->is_type('variable')) { WC_Product_Variable::sync($product_id); wc_delete_product_transients($product_id); } wc_add_notice('تنوع با موفقیت ساخته شد.', 'success'); } add_action('template_redirect', 'mvx_handle_form_submit'); /** * ---------- Front Form ---------- */ function mvx_render_quick_variation_form() { if (!is_product()) return; if (!mvx_is_admin_user()) return; global $product; if (!$product || !is_a($product, 'WC_Product')) return; $attributes = mvx_get_product_all_attributes($product); if (empty($attributes)) return; $product_id = $product->get_id(); ?> <div class="mvx-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;"> <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3> <form method="post" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <?php wp_nonce_field('mvx_quick_variation_action', 'mvx_quick_variation_nonce'); ?> <input type="hidden" name="mvx_product_id" value="<?php echo esc_attr($product_id); ?>"> <div> <label style="display:block;margin-bottom:6px;">ویژگی اول</label> <select name="mvx_attr1" id="mvx_attr1" style="width:100%;padding:8px;"> <option value="">انتخاب ویژگی</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label> <select name="mvx_val1" id="mvx_val1" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div> <label style="display:block;margin-bottom:6px;">ویژگی دوم</label> <select name="mvx_attr2" id="mvx_attr2" style="width:100%;padding:8px;"> <option value="">بدون ویژگی دوم</option> <?php foreach ($attributes as $attr): ?> <option value="<?php echo esc_attr($attr['name']); ?>"> <?php echo esc_html($attr['label']); ?> </option> <?php endforeach; ?> </select> </div> <div> <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label> <select name="mvx_val2" id="mvx_val2" style="width:100%;padding:8px;"> <option value="">ابتدا ویژگی را انتخاب کنید</option> </select> </div> <div style="grid-column:1 / -1;"> <label style="display:block;margin-bottom:6px;">قیمت</label> <input type="number" step="0.01" min="0" name="mvx_price" style="width:100%;padding:8px;" required> </div> <div style="grid-column:1 / -1;"> <button type="submit" name="mvx_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;"> افزودن تنوع </button> </div> </form> </div> <script> (function(){ var data = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>; var attr1 = document.getElementById('mvx_attr1'); var val1 = document.getElementById('mvx_val1'); var attr2 = document.getElementById('mvx_attr2'); var val2 = document.getElementById('mvx_val2'); function getAttr(name){ for (var i = 0; i < data.length; i++) { if (data[i].name === name) return data[i]; } return null; } function refill(attrSelect, valSelect, allowEmptyLabel){ var attrName = attrSelect.value; valSelect.innerHTML = ''; if (!attrName) { var op = document.createElement('option'); op.value = ''; op.textContent = allowEmptyLabel || 'ابتدا ویژگی را انتخاب کنید'; valSelect.appendChild(op); return; } var attr = getAttr(attrName); var items = (attr && attr.options) ? attr.options : []; var first = document.createElement('option'); first.value = ''; first.textContent = 'انتخاب مقدار'; valSelect.appendChild(first); var any = document.createElement('option'); any.value = '__any__'; any.textContent = 'همه موارد'; valSelect.appendChild(any); items.forEach(function(item){ var op = document.createElement('option'); op.value = item.value; op.textContent = item.label; valSelect.appendChild(op); }); } attr1.addEventListener('change', function(){ refill(attr1, val1, 'ابتدا ویژگی را انتخاب کنید'); if (attr2.value && attr2.value === attr1.value) { attr2.value = ''; refill(attr2, val2, 'ابتدا ویژگی را انتخاب کنید'); } }); attr2.addEventListener('change', function(){ if (attr1.value && attr2.value && attr1.value === attr2.value) { alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.'); attr2.value = ''; } refill(attr2, val2, 'ابتدا ویژگی را انتخاب کنید'); }); })(); </script> <?php } add_action('woocommerce_single_product_summary', 'mvx_render_quick_variation_form', 35); /** * ---------- Optional: hide weird encoded attribute text in some themes ---------- */ add_filter('woocommerce_get_item_data', function($item_data, $cart_item) { if (empty($item_data) || !is_array($item_data)) return $item_data; foreach ($item_data as $index => $item) { if (!empty($item['value']) && is_string($item['value'])) { $item_data[$index]['value'] = rawurldecode(wp_specialchars_decode($item['value'], ENT_QUOTES)); } if (!empty($item['display']) && is_string($item['display'])) { $item_data[$index]['display'] = rawurldecode(wp_specialchars_decode($item['display'], ENT_QUOTES)); } } return $item_data; }, 999, 2); add_filter('woocommerce_cart_item_name', function($name, $cart_item, $cart_item_key) { if (is_string($name)) { $name = rawurldecode(wp_specialchars_decode($name, ENT_QUOTES)); } return $name; }, 999, 3);
<?php
if (!defined('ABSPATH')) exit;

/**
 * ============================================
 *  Quick Add WooCommerce Variation (Final)
 *  - Admin only on single product page
 *  - Supports global/local attributes
 *  - Supports Persian values
 *  - Prevents duplicate variations
 *  - Supports "Any / همه موارد"
 * ============================================
 */

/**
 * ---------- Helpers ----------
 */

function mvx_normalize_text($value) {
    $value = is_string($value) ? wp_unslash($value) : $value;
    $value = wc_clean($value);
    return trim((string) $value);
}

function mvx_is_admin_user() {
    return current_user_can('manage_woocommerce') || current_user_can('administrator');
}

function mvx_get_product_all_attributes($product) {
    if (!$product || !is_a($product, 'WC_Product')) return array();

    $result = array();
    $attributes = $product->get_attributes();

    foreach ($attributes as $key => $attribute) {
        if (!is_a($attribute, 'WC_Product_Attribute')) continue;

        $name = $attribute->get_name();
        $label = wc_attribute_label($name);
        $is_taxonomy = $attribute->is_taxonomy();

        $options = array();

        if ($is_taxonomy && taxonomy_exists($name)) {
            $terms = wc_get_product_terms($product->get_id(), $name, array('fields' => 'all'));
            if (!empty($terms) && !is_wp_error($terms)) {
                foreach ($terms as $term) {
                    $options[] = array(
                        'value' => (string) $term->slug,
                        'label' => (string) $term->name,
                    );
                }
            }
        } else {
            $raw_options = $attribute->get_options();
            if (!empty($raw_options)) {
                foreach ($raw_options as $opt) {
                    $opt = (string) $opt;
                    $options[] = array(
                        'value' => $opt,
                        'label' => $opt,
                    );
                }
            }
        }

        $result[] = array(
            'name'        => $name,
            'label'       => $label ? $label : $name,
            'is_taxonomy' => $is_taxonomy,
            'options'     => $options,
        );
    }

    return $result;
}

function mvx_find_attribute_def($product, $attr_name) {
    $all = mvx_get_product_all_attributes($product);
    foreach ($all as $attr) {
        if ($attr['name'] === $attr_name) {
            return $attr;
        }
    }
    return false;
}

function mvx_get_term_from_posted_value($taxonomy, $posted_value) {
    if (!taxonomy_exists($taxonomy)) return false;

    $posted_value = mvx_normalize_text($posted_value);
    if ($posted_value === '') return false;

    $term = get_term_by('slug', $posted_value, $taxonomy);
    if ($term && !is_wp_error($term)) return $term;

    $terms = get_terms(array(
        'taxonomy'   => $taxonomy,
        'hide_empty' => false,
    ));

    if (!is_wp_error($terms) && !empty($terms)) {
        foreach ($terms as $t) {
            if ((string) $t->name === (string) $posted_value) {
                return $t;
            }
        }
    }

    return false;
}

function mvx_prepare_variation_value($attr_def, $raw_value) {
    if ($raw_value === '__any__') {
        return array(
            'meta_value'      => '',
            'variation_value' => '',
            'display_value'   => 'همه موارد',
        );
    }

    if (!$attr_def) {
        return array(
            'meta_value'      => $raw_value,
            'variation_value' => $raw_value,
            'display_value'   => $raw_value,
        );
    }

    if (!empty($attr_def['is_taxonomy'])) {
        $taxonomy = $attr_def['name'];
        $term = mvx_get_term_from_posted_value($taxonomy, $raw_value);

        if ($term) {
            return array(
                'meta_value'      => $term->slug,
                'variation_value' => $term->slug,
                'display_value'   => $term->name,
            );
        }

        return array(
            'meta_value'      => '',
            'variation_value' => '',
            'display_value'   => '',
        );
    }

    return array(
        'meta_value'      => (string) $raw_value,
        'variation_value' => (string) $raw_value,
        'display_value'   => (string) $raw_value,
    );
}

function mvx_add_option_to_product_attribute($product_id, $attr_name, $posted_value, $attr_def = false) {
    if ($posted_value === '__any__') {
        return true;
    }

    $product = wc_get_product($product_id);
    if (!$product) return false;

    $attributes = $product->get_attributes();
    $found = false;

    foreach ($attributes as $key => $attribute) {
        if (!is_a($attribute, 'WC_Product_Attribute')) continue;
        if ($attribute->get_name() !== $attr_name) continue;

        $found = true;

        if ($attribute->is_taxonomy()) {
            $term = false;

            if ($attr_def && !empty($attr_def['is_taxonomy'])) {
                $term = mvx_get_term_from_posted_value($attr_name, $posted_value);
            }

            if ($term) {
                wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true);

                $current_options = (array) $attribute->get_options();
                $current_options = array_map('intval', $current_options);

                $term_id = (int) $term->term_id;

                if (!in_array($term_id, $current_options, true)) {
                    $current_options[] = $term_id;
                }

                $attribute->set_options($current_options);
            }

            $attribute->set_id(wc_attribute_taxonomy_id_by_name($attr_name));
            $attribute->set_visible(true);
            $attribute->set_variation(true);
            $attributes[$key] = $attribute;

        } else {
            $attr_value = (string) $posted_value;
            $options = (array) $attribute->get_options();

            if (!in_array($attr_value, $options, true)) {
                $options[] = $attr_value;
                $attribute->set_options($options);
            }

            $attribute->set_visible(true);
            $attribute->set_variation(true);
            $attributes[$key] = $attribute;
        }
    }

    if (!$found) {
        $new_attr = new WC_Product_Attribute();
        $new_attr->set_name($attr_name);
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
        $new_attr->set_position(count($attributes));

        if ($attr_def && !empty($attr_def['is_taxonomy']) && taxonomy_exists($attr_name)) {
            $term = mvx_get_term_from_posted_value($attr_name, $posted_value);

            $new_attr->set_id(wc_attribute_taxonomy_id_by_name($attr_name));

            if ($term) {
                wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true);
                $new_attr->set_options(array((int) $term->term_id));
            } else {
                $new_attr->set_options(array());
            }
        } else {
            $attr_value = (string) $posted_value;
            $new_attr->set_options(array($attr_value));
        }

        $attributes[$attr_name] = $new_attr;
    }

    $product->set_attributes($attributes);
    $product->save();

    return true;
}

function mvx_make_product_variable_if_needed($product_id) {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    if ($product->is_type('variable')) {
        return $product;
    }

    wp_set_object_terms($product_id, 'variable', 'product_type');

    delete_transient('wc_product_children_' . $product_id);
    wc_delete_product_transients($product_id);

    return wc_get_product($product_id);
}

function mvx_get_variation_signature($attrs) {
    ksort($attrs);
    return md5(wp_json_encode($attrs, JSON_UNESCAPED_UNICODE));
}

function mvx_variation_exists($product_id, $candidate_attrs) {
    $product = wc_get_product($product_id);
    if (!$product || !$product->is_type('variable')) return false;

    $children = $product->get_children();
    if (empty($children)) return false;

    $candidate_signature = mvx_get_variation_signature($candidate_attrs);

    foreach ($children as $child_id) {
        $variation = wc_get_product($child_id);
        if (!$variation || !is_a($variation, 'WC_Product_Variation')) continue;

        $existing = $variation->get_attributes();
        $normalized = array();

        foreach ($existing as $k => $v) {
            $tax_key = strpos($k, 'attribute_') === 0 ? $k : 'attribute_' . $k;
            $clean_key = str_replace('attribute_', '', $tax_key);
            $normalized[$clean_key] = (string) $v;
        }

        if (mvx_get_variation_signature($normalized) === $candidate_signature) {
            return $child_id;
        }
    }

    return false;
}

function mvx_create_variation($product_id, $attrs_for_variation, $regular_price) {
    $existing_id = mvx_variation_exists($product_id, $attrs_for_variation);
    if ($existing_id) {
        return new WP_Error('variation_exists', 'این تنوع قبلاً وجود دارد.');
    }

    $variation_post = array(
        'post_title'  => 'Product Variation',
        'post_name'   => 'product-' . $product_id . '-variation-' . time() . '-' . wp_rand(100, 999),
        'post_status' => 'publish',
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'guid'        => home_url('/?product_variation=product-' . $product_id),
    );

    $variation_id = wp_insert_post($variation_post);

    if (is_wp_error($variation_id) || !$variation_id) {
        return new WP_Error('variation_create_failed', 'خطا در ساخت تنوع.');
    }

    $variation = new WC_Product_Variation($variation_id);

    foreach ($attrs_for_variation as $taxonomy => $value) {
        update_post_meta($variation_id, 'attribute_' . $taxonomy, $value);
    }

    $variation->set_props(array(
        'regular_price' => wc_format_decimal($regular_price),
        'price'         => wc_format_decimal($regular_price),
    ));

    $variation->save();

    wc_delete_product_transients($product_id);

    return $variation_id;
}

/**
 * جلوگیری از نمایش عجیب مقادیر variation در فرانت
 */
add_filter('woocommerce_variation_option_name', function($term_name) {
    if (!is_string($term_name)) return $term_name;

    $decoded = rawurldecode($term_name);
    $decoded = wp_specialchars_decode($decoded, ENT_QUOTES);

    return $decoded;
}, 999);

add_filter('woocommerce_product_variation_title_include_attributes', '__return_false', 999);

add_filter('woocommerce_available_variation', function($data, $product, $variation) {
    if (!empty($data['attributes']) && is_array($data['attributes'])) {
        foreach ($data['attributes'] as $k => $v) {
            if (is_string($v)) {
                $data['attributes'][$k] = rawurldecode($v);
            }
        }
    }

    if (!empty($data['variation_description']) && is_string($data['variation_description'])) {
        $data['variation_description'] = rawurldecode($data['variation_description']);
    }

    return $data;
}, 999, 3);

/**
 * ---------- Form Submit ----------
 */

function mvx_handle_form_submit() {
    if (!isset($_POST['mvx_quick_variation_submit'])) return;
    if (!mvx_is_admin_user()) return;

    if (!isset($_POST['mvx_quick_variation_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['mvx_quick_variation_nonce'])), 'mvx_quick_variation_action')) {
        wc_add_notice('اعتبارسنجی ناموفق بود.', 'error');
        return;
    }

    $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0;
    $attr1      = isset($_POST['mvx_attr1']) ? mvx_normalize_text($_POST['mvx_attr1']) : '';
    $val1       = isset($_POST['mvx_val1']) ? mvx_normalize_text($_POST['mvx_val1']) : '';
    $attr2      = isset($_POST['mvx_attr2']) ? mvx_normalize_text($_POST['mvx_attr2']) : '';
    $val2       = isset($_POST['mvx_val2']) ? mvx_normalize_text($_POST['mvx_val2']) : '';
    $price      = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : '';

    if (!$product_id || !$attr1 || $val1 === '') {
        wc_add_notice('لطفاً ویژگی اول و مقدار آن را انتخاب کنید.', 'error');
        return;
    }

    if ($price === '') {
        wc_add_notice('لطفاً قیمت را وارد کنید.', 'error');
        return;
    }

    if ($attr2 && !$val2 && $val2 !== '0') {
        wc_add_notice('برای ویژگی دوم باید مقدار انتخاب شود.', 'error');
        return;
    }

    if ($attr1 && $attr2 && $attr1 === $attr2) {
        wc_add_notice('ویژگی اول و دوم نمی‌توانند یکسان باشند.', 'error');
        return;
    }

    if ($val1 === '__any__' && $val2 === '__any__') {
        wc_add_notice('نمی‌توان برای هر دو ویژگی "همه موارد" انتخاب کرد.', 'error');
        return;
    }

    $product = mvx_make_product_variable_if_needed($product_id);
    if (!$product) {
        wc_add_notice('محصول پیدا نشد.', 'error');
        return;
    }

    $attr1_def = mvx_find_attribute_def($product, $attr1);
    $attr2_def = $attr2 ? mvx_find_attribute_def($product, $attr2) : false;

    if (!$attr1_def) {
        wc_add_notice('ویژگی اول در محصول یافت نشد.', 'error');
        return;
    }

    if ($attr2 && !$attr2_def) {
        wc_add_notice('ویژگی دوم در محصول یافت نشد.', 'error');
        return;
    }

    mvx_add_option_to_product_attribute($product_id, $attr1, $val1, $attr1_def);

    if ($attr2 && $val2 !== '') {
        mvx_add_option_to_product_attribute($product_id, $attr2, $val2, $attr2_def);
    }

    $prepared1 = mvx_prepare_variation_value($attr1_def, $val1);

    $variation_attrs = array(
        $attr1 => $prepared1['variation_value'],
    );

    if ($attr2 && $val2 !== '') {
        $prepared2 = mvx_prepare_variation_value($attr2_def, $val2);
        $variation_attrs[$attr2] = $prepared2['variation_value'];
    }

    $created = mvx_create_variation($product_id, $variation_attrs, $price);

    if (is_wp_error($created)) {
        wc_add_notice($created->get_error_message(), 'error');
        return;
    }

    $product = wc_get_product($product_id);
    if ($product && $product->is_type('variable')) {
        WC_Product_Variable::sync($product_id);
        wc_delete_product_transients($product_id);
    }

    wc_add_notice('تنوع با موفقیت ساخته شد.', 'success');
}
add_action('template_redirect', 'mvx_handle_form_submit');

/**
 * ---------- Front Form ----------
 */

function mvx_render_quick_variation_form() {
    if (!is_product()) return;
    if (!mvx_is_admin_user()) return;

    global $product;
    if (!$product || !is_a($product, 'WC_Product')) return;

    $attributes = mvx_get_product_all_attributes($product);
    if (empty($attributes)) return;

    $product_id = $product->get_id();
    ?>
    <div class="mvx-quick-variation-box" style="margin:20px 0;padding:15px;border:1px solid #ddd;background:#fafafa;border-radius:8px;">
        <h3 style="margin-top:0;margin-bottom:15px;">افزودن سریع تنوع</h3>

        <form method="post" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
            <?php wp_nonce_field('mvx_quick_variation_action', 'mvx_quick_variation_nonce'); ?>
            <input type="hidden" name="mvx_product_id" value="<?php echo esc_attr($product_id); ?>">

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی اول</label>
                <select name="mvx_attr1" id="mvx_attr1" style="width:100%;padding:8px;">
                    <option value="">انتخاب ویژگی</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی اول</label>
                <select name="mvx_val1" id="mvx_val1" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">ویژگی دوم</label>
                <select name="mvx_attr2" id="mvx_attr2" style="width:100%;padding:8px;">
                    <option value="">بدون ویژگی دوم</option>
                    <?php foreach ($attributes as $attr): ?>
                        <option value="<?php echo esc_attr($attr['name']); ?>">
                            <?php echo esc_html($attr['label']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div>
                <label style="display:block;margin-bottom:6px;">مقدار ویژگی دوم</label>
                <select name="mvx_val2" id="mvx_val2" style="width:100%;padding:8px;">
                    <option value="">ابتدا ویژگی را انتخاب کنید</option>
                </select>
            </div>

            <div style="grid-column:1 / -1;">
                <label style="display:block;margin-bottom:6px;">قیمت</label>
                <input type="number" step="0.01" min="0" name="mvx_price" style="width:100%;padding:8px;" required>
            </div>

            <div style="grid-column:1 / -1;">
                <button type="submit" name="mvx_quick_variation_submit" value="1" style="padding:10px 18px;background:#2271b1;color:#fff;border:none;border-radius:6px;cursor:pointer;">
                    افزودن تنوع
                </button>
            </div>
        </form>
    </div>

    <script>
    (function(){
        var data = <?php echo wp_json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;

        var attr1 = document.getElementById('mvx_attr1');
        var val1  = document.getElementById('mvx_val1');
        var attr2 = document.getElementById('mvx_attr2');
        var val2  = document.getElementById('mvx_val2');

        function getAttr(name){
            for (var i = 0; i < data.length; i++) {
                if (data[i].name === name) return data[i];
            }
            return null;
        }

        function refill(attrSelect, valSelect, allowEmptyLabel){
            var attrName = attrSelect.value;
            valSelect.innerHTML = '';

            if (!attrName) {
                var op = document.createElement('option');
                op.value = '';
                op.textContent = allowEmptyLabel || 'ابتدا ویژگی را انتخاب کنید';
                valSelect.appendChild(op);
                return;
            }

            var attr = getAttr(attrName);
            var items = (attr && attr.options) ? attr.options : [];

            var first = document.createElement('option');
            first.value = '';
            first.textContent = 'انتخاب مقدار';
            valSelect.appendChild(first);

            var any = document.createElement('option');
            any.value = '__any__';
            any.textContent = 'همه موارد';
            valSelect.appendChild(any);

            items.forEach(function(item){
                var op = document.createElement('option');
                op.value = item.value;
                op.textContent = item.label;
                valSelect.appendChild(op);
            });
        }

        attr1.addEventListener('change', function(){
            refill(attr1, val1, 'ابتدا ویژگی را انتخاب کنید');

            if (attr2.value && attr2.value === attr1.value) {
                attr2.value = '';
                refill(attr2, val2, 'ابتدا ویژگی را انتخاب کنید');
            }
        });

        attr2.addEventListener('change', function(){
            if (attr1.value && attr2.value && attr1.value === attr2.value) {
                alert('ویژگی اول و دوم نمی‌توانند یکسان باشند.');
                attr2.value = '';
            }
            refill(attr2, val2, 'ابتدا ویژگی را انتخاب کنید');
        });
    })();
    </script>
    <?php
}
add_action('woocommerce_single_product_summary', 'mvx_render_quick_variation_form', 35);

/**
 * ---------- Optional: hide weird encoded attribute text in some themes ----------
 */

add_filter('woocommerce_get_item_data', function($item_data, $cart_item) {
    if (empty($item_data) || !is_array($item_data)) return $item_data;

    foreach ($item_data as $index => $item) {
        if (!empty($item['value']) && is_string($item['value'])) {
            $item_data[$index]['value'] = rawurldecode(wp_specialchars_decode($item['value'], ENT_QUOTES));
        }
        if (!empty($item['display']) && is_string($item['display'])) {
            $item_data[$index]['display'] = rawurldecode(wp_specialchars_decode($item['display'], ENT_QUOTES));
        }
    }

    return $item_data;
}, 999, 2);

add_filter('woocommerce_cart_item_name', function($name, $cart_item, $cart_item_key) {
    if (is_string($name)) {
        $name = rawurldecode(wp_specialchars_decode($name, ENT_QUOTES));
    }
    return $name;
}, 999, 3);
تنوع ۱۷
TEXT - 2026-05-05 22:30:50
<?php if (!defined('ABSPATH')) exit; /* ========================= * دسترسی * ========================= */ function mvx_user_can_manage() { return is_user_logged_in() && ( current_user_can('manage_options') || current_user_can('manage_woocommerce') || current_user_can('edit_products') ); } /* ========================= * گرفتن محصول جاری * ========================= */ function mvx_get_current_product() { if (!function_exists('is_product') || !is_product()) return false; $product_id = get_queried_object_id(); if (!$product_id) return false; $product = wc_get_product($product_id); return ($product && is_a($product, 'WC_Product')) ? $product : false; } /* ========================= * همه attribute ها برای فرم * - global => value = term_id * - local => value = خود متن * ========================= */ function mvx_get_all_attributes_for_form($product = false) { $result = array(); /* global attributes */ if (function_exists('wc_get_attribute_taxonomies')) { $taxonomies = wc_get_attribute_taxonomies(); if (!empty($taxonomies)) { foreach ($taxonomies as $tax) { $taxonomy_name = wc_attribute_taxonomy_name($tax->attribute_name); // pa_color if (!taxonomy_exists($taxonomy_name)) continue; $terms = get_terms(array( 'taxonomy' => $taxonomy_name, 'hide_empty' => false, )); $options = array(); if (!is_wp_error($terms) && !empty($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => (string) $term->term_id, 'label' => $term->name, 'term_id' => (int) $term->term_id, 'slug' => $term->slug, 'name' => $term->name, ); } } $result[$taxonomy_name] = array( 'name' => $taxonomy_name, 'label' => $tax->attribute_label ? $tax->attribute_label : $tax->attribute_name, 'is_taxonomy' => true, 'options' => $options, 'source' => 'global', ); } } } /* local product attributes */ if ($product) { $product_attributes = $product->get_attributes(); if (!empty($product_attributes)) { foreach ($product_attributes as $key => $attribute) { if (!is_a($attribute, 'WC_Product_Attribute')) continue; if ($attribute->is_taxonomy()) continue; $name = $attribute->get_name(); $label = wc_attribute_label($name, $product); $options = array(); foreach ((array) $attribute->get_options() as $opt) { $opt = (string) $opt; $options[] = array( 'value' => $opt, 'label' => $opt, ); } $result[$name] = array( 'name' => $name, 'label' => $label ? $label : $name, 'is_taxonomy' => false, 'options' => $options, 'source' => 'local', ); } } } return $result; } /* ========================= * پیدا کردن تعریف attribute * ========================= */ function mvx_find_attribute_definition($all_attrs, $attr_name) { return isset($all_attrs[$attr_name]) ? $all_attrs[$attr_name] : false; } /* ========================= * پیدا کردن term از روی term_id * ========================= */ function mvx_get_term_from_posted_value($taxonomy, $posted_value) { $term_id = absint($posted_value); if (!$term_id) return false; $term = get_term($term_id, $taxonomy); if ($term && !is_wp_error($term)) { return $term; } return false; } /* ========================= * افزودن option به attribute محصول * اصلاح‌شده: * برای global attribute، term_id داخل options محصول هم ذخیره می‌شود * تا variation روی "همه موارد" نماند * ========================= */ function mvx_add_option_to_product_attribute($product_id, $attr_name, $posted_value, $attr_def = false) { $product = wc_get_product($product_id); if (!$product) return false; $attributes = $product->get_attributes(); $found = false; foreach ($attributes as $key => $attribute) { if (!is_a($attribute, 'WC_Product_Attribute')) continue; if ($attribute->get_name() !== $attr_name) continue; $found = true; if ($attribute->is_taxonomy()) { $term = false; if ($attr_def && !empty($attr_def['is_taxonomy'])) { $term = mvx_get_term_from_posted_value($attr_name, $posted_value); } if ($term) { wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true); $current_options = (array) $attribute->get_options(); $current_options = array_map('intval', $current_options); $term_id = (int) $term->term_id; if (!in_array($term_id, $current_options, true)) { $current_options[] = $term_id; } $attribute->set_options($current_options); } $attribute->set_id(wc_attribute_taxonomy_id_by_name($attr_name)); $attribute->set_visible(true); $attribute->set_variation(true); $attributes[$key] = $attribute; } else { $attr_value = (string) $posted_value; $options = (array) $attribute->get_options(); if (!in_array($attr_value, $options, true)) { $options[] = $attr_value; $attribute->set_options($options); } $attribute->set_visible(true); $attribute->set_variation(true); $attributes[$key] = $attribute; } } if (!$found) { $new_attr = new WC_Product_Attribute(); $new_attr->set_name($attr_name); $new_attr->set_visible(true); $new_attr->set_variation(true); $new_attr->set_position(count($attributes)); if ($attr_def && !empty($attr_def['is_taxonomy']) && taxonomy_exists($attr_name)) { $term = mvx_get_term_from_posted_value($attr_name, $posted_value); $new_attr->set_id(wc_attribute_taxonomy_id_by_name($attr_name)); if ($term) { wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true); // نکته مهم: اینجا نباید خالی باشد // باید term_id داخل options ذخیره شود $new_attr->set_options(array((int) $term->term_id)); } else { $new_attr->set_options(array()); } } else { $attr_value = (string) $posted_value; $new_attr->set_options(array($attr_value)); } $attributes[$attr_name] = $new_attr; } $product->set_attributes($attributes); $product->save(); return true; } /* ========================= * آماده‌سازی مقدار variation * global => slug term * local => text * ========================= */ function mvx_prepare_variation_value($attr_def, $raw_value) { if (!$attr_def) { return array( 'meta_value' => $raw_value, 'variation_value' => $raw_value, 'display_value' => $raw_value, ); } if (!empty($attr_def['is_taxonomy'])) { $taxonomy = $attr_def['name']; $term = mvx_get_term_from_posted_value($taxonomy, $raw_value); if ($term) { return array( 'meta_value' => $term->slug, 'variation_value' => $term->slug, 'display_value' => $term->name, ); } return array( 'meta_value' => '', 'variation_value' => '', 'display_value' => '', ); } return array( 'meta_value' => (string) $raw_value, 'variation_value' => (string) $raw_value, 'display_value' => (string) $raw_value, ); } /* ========================= * آیا variation وجود دارد؟ * ========================= */ function mvx_variation_exists($product_id, $attrs_meta) { $children = get_posts(array( 'post_parent' => $product_id, 'post_type' => 'product_variation', 'post_status' => array('publish', 'private'), 'numberposts' => -1, 'fields' => 'ids', )); foreach ($children as $variation_id) { $matched = true; foreach ($attrs_meta as $meta_key => $meta_value) { $saved = get_post_meta($variation_id, $meta_key, true); if ((string) $saved !== (string) $meta_value) { $matched = false; break; } } if ($matched) return true; } return false; } /* ========================= * simple -> variable * ========================= */ function mvx_convert_simple_to_variable($product_id) { wp_set_object_terms($product_id, 'variable', 'product_type'); wc_delete_product_transients($product_id); return wc_get_product($product_id); } /* ========================= * پردازش فرم * ========================= */ function mvx_handle_form_submit() { if (is_admin()) return; if (!mvx_user_can_manage()) return; if (empty($_POST['mvx_action']) || $_POST['mvx_action'] !== 'add_variation_two_rows') return; if (empty($_POST['mvx_nonce']) || !wp_verify_nonce($_POST['mvx_nonce'], 'mvx_add_variation_two_rows')) { return; } $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0; $attr1 = isset($_POST['mvx_attr1']) ? wc_clean(wp_unslash($_POST['mvx_attr1'])) : ''; $val1 = isset($_POST['mvx_val1']) ? wp_unslash($_POST['mvx_val1']) : ''; $attr2 = isset($_POST['mvx_attr2']) ? wc_clean(wp_unslash($_POST['mvx_attr2'])) : ''; $val2 = isset($_POST['mvx_val2']) ? wp_unslash($_POST['mvx_val2']) : ''; $price = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : ''; if (!$product_id || !$attr1 || $val1 === '') { wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id))); exit; } if ($attr1 && $attr2 && $attr1 === $attr2) { wp_safe_redirect(add_query_arg('mvx_msg', 'sameattr', get_permalink($product_id))); exit; } $product = wc_get_product($product_id); if (!$product) return; if ($product->get_type() === 'simple') { $product = mvx_convert_simple_to_variable($product_id); } if (!$product || $product->get_type() !== 'variable') { wp_safe_redirect(add_query_arg('mvx_msg', 'notvariable', get_permalink($product_id))); exit; } $all_attrs = mvx_get_all_attributes_for_form($product); $def1 = mvx_find_attribute_definition($all_attrs, $attr1); $def2 = $attr2 ? mvx_find_attribute_definition($all_attrs, $attr2) : false; if (!$def1) { wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id))); exit; } if ($attr2 && !$def2) { wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id))); exit; } mvx_add_option_to_product_attribute($product_id, $attr1, $val1, $def1); if ($attr2 && $val2 !== '') { mvx_add_option_to_product_attribute($product_id, $attr2, $val2, $def2); } // بعد از ذخیره attributeهای محصول، محصول را تازه می‌کنیم $product = wc_get_product($product_id); $prepared1 = mvx_prepare_variation_value($def1, $val1); if ($prepared1['meta_value'] === '') { wp_safe_redirect(add_query_arg('mvx_msg', 'badvalue', get_permalink($product_id))); exit; } $variation_meta = array( 'attribute_' . $attr1 => $prepared1['meta_value'], ); $variation_set_attrs = array( $attr1 => $prepared1['variation_value'], ); if ($attr2 && $val2 !== '') { $prepared2 = mvx_prepare_variation_value($def2, $val2); if ($prepared2['meta_value'] === '') { wp_safe_redirect(add_query_arg('mvx_msg', 'badvalue', get_permalink($product_id))); exit; } $variation_meta['attribute_' . $attr2] = $prepared2['meta_value']; $variation_set_attrs[$attr2] = $prepared2['variation_value']; } if (mvx_variation_exists($product_id, $variation_meta)) { wp_safe_redirect(add_query_arg('mvx_msg', 'exists', get_permalink($product_id))); exit; } $variation = new WC_Product_Variation(); $variation->set_parent_id($product_id); $variation->set_status('publish'); $variation->set_attributes($variation_set_attrs); if ($price !== '') { $variation->set_regular_price($price); $variation->set_price($price); } $variation_id = $variation->save(); if (!$variation_id || is_wp_error($variation_id)) { wp_safe_redirect(add_query_arg('mvx_msg', 'error', get_permalink($product_id))); exit; } foreach ($variation_meta as $meta_key => $meta_value) { update_post_meta($variation_id, $meta_key, $meta_value); } update_post_meta($variation_id, '_virtual', 'no'); update_post_meta($variation_id, '_downloadable', 'no'); WC_Product_Variable::sync($product_id); wc_delete_product_transients($product_id); wp_safe_redirect(add_query_arg(array( 'mvx_msg' => 'created', 'mvx_vid' => $variation_id, ), get_permalink($product_id))); exit; } add_action('template_redirect', 'mvx_handle_form_submit'); /* ========================= * UI * ========================= */ function mvx_render_variation_box() { if (is_admin()) return; if (!function_exists('is_product') || !is_product()) return; if (!mvx_user_can_manage()) return; $product = mvx_get_current_product(); if (!$product) return; $attrs_assoc = mvx_get_all_attributes_for_form($product); $attrs = array_values($attrs_assoc); echo '<div id="mvx-box" style="position:fixed;left:20px;bottom:20px;width:560px;max-width:calc(100vw - 30px);z-index:999999;background:#fff;border:2px solid #2563eb;border-radius:14px;box-shadow:0 12px 35px rgba(0,0,0,.18);padding:14px;direction:rtl;text-align:right;">'; echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">'; echo '<strong style="font-size:15px;">افزودن تنوع</strong>'; echo '<button type="button" onclick="document.getElementById(\'mvx-box\').style.display=\'none\';" style="background:#e5e7eb;border:none;border-radius:8px;padding:2px 8px;cursor:pointer;">×</button>'; echo '</div>'; if (isset($_GET['mvx_msg'])) { $msg = sanitize_text_field(wp_unslash($_GET['mvx_msg'])); $style_ok = 'background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;padding:8px 10px;border-radius:10px;margin-bottom:10px;'; $style_bad = 'background:#fef2f2;color:#991b1b;border:1px solid #fecaca;padding:8px 10px;border-radius:10px;margin-bottom:10px;'; $style_wrn = 'background:#fff7ed;color:#9a3412;border:1px solid #fdba74;padding:8px 10px;border-radius:10px;margin-bottom:10px;'; if ($msg === 'created') { echo '<div style="'.$style_ok.'">تنوع ساخته شد.</div>'; } elseif ($msg === 'exists') { echo '<div style="'.$style_wrn.'">این ترکیب از قبل وجود دارد.</div>'; } elseif ($msg === 'sameattr') { echo '<div style="'.$style_bad.'">ویژگی ردیف اول و دوم نباید یکی باشد.</div>'; } elseif ($msg === 'missing') { echo '<div style="'.$style_bad.'">ویژگی و مقدار معتبر الزامی است.</div>'; } elseif ($msg === 'notvariable') { echo '<div style="'.$style_bad.'">محصول به variable تبدیل نشد.</div>'; } elseif ($msg === 'badvalue') { echo '<div style="'.$style_bad.'">مقدار انتخابی معتبر نیست.</div>'; } elseif ($msg === 'error') { echo '<div style="'.$style_bad.'">خطا در ساخت variation.</div>'; } } echo '<form method="post">'; echo wp_nonce_field('mvx_add_variation_two_rows', 'mvx_nonce', true, false); echo '<input type="hidden" name="mvx_action" value="add_variation_two_rows">'; echo '<input type="hidden" name="mvx_product_id" value="' . esc_attr($product->get_id()) . '">'; echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;">'; echo '<div>'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">ویژگی</label>'; echo '<select id="mvx_attr1" name="mvx_attr1" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '<option value="">انتخاب ویژگی</option>'; foreach ($attrs as $a) { echo '<option value="' . esc_attr($a['name']) . '">' . esc_html($a['label']) . '</option>'; } echo '</select>'; echo '</div>'; echo '<div>'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">مقدار</label>'; echo '<select id="mvx_val1" name="mvx_val1" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '<option value="">ابتدا ویژگی را انتخاب کنید</option>'; echo '</select>'; echo '</div>'; echo '</div>'; echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;">'; echo '<div>'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">ویژگی</label>'; echo '<select id="mvx_attr2" name="mvx_attr2" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '<option value="">انتخاب ویژگی</option>'; foreach ($attrs as $a) { echo '<option value="' . esc_attr($a['name']) . '">' . esc_html($a['label']) . '</option>'; } echo '</select>'; echo '</div>'; echo '<div>'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">مقدار</label>'; echo '<select id="mvx_val2" name="mvx_val2" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '<option value="">ابتدا ویژگی را انتخاب کنید</option>'; echo '</select>'; echo '</div>'; echo '</div>'; echo '<div style="margin-bottom:12px;">'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">قیمت</label>'; echo '<input type="number" step="any" name="mvx_price" placeholder="مثلاً 350000" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '</div>'; echo '<button type="submit" onclick="return confirm(\'تنوع اضافه شود؟\');" style="width:100%;background:#2563eb;color:#fff;border:none;border-radius:10px;padding:12px;cursor:pointer;font-weight:700;">افزودن تنوع</button>'; echo '</form>'; $map = array(); foreach ($attrs as $a) { $map[$a['name']] = $a['options']; } echo '<script>'; echo 'window.mvxAttrMap = ' . wp_json_encode($map) . ';'; ?> (function(){ function bind(attrId, valId, otherAttrId){ var attr = document.getElementById(attrId); var val = document.getElementById(valId); var other = document.getElementById(otherAttrId); if(!attr || !val) return; function refill(){ val.innerHTML = ''; var selected = attr.value; if(other && other.value && selected && other.value === selected){ alert('این ویژگی در ردیف دیگر انتخاب شده است.'); attr.value = ''; selected = ''; } if(!selected){ var op = document.createElement('option'); op.value = ''; op.textContent = 'ابتدا ویژگی را انتخاب کنید'; val.appendChild(op); return; } var items = (window.mvxAttrMap && window.mvxAttrMap[selected]) ? window.mvxAttrMap[selected] : []; if(!items.length){ var op2 = document.createElement('option'); op2.value = ''; op2.textContent = 'مقداری یافت نشد'; val.appendChild(op2); return; } var first = document.createElement('option'); first.value = ''; first.textContent = 'انتخاب مقدار'; val.appendChild(first); items.forEach(function(item){ var op3 = document.createElement('option'); op3.value = item.value; op3.textContent = item.label; val.appendChild(op3); }); } attr.addEventListener('change', refill); refill(); } bind('mvx_attr1', 'mvx_val1', 'mvx_attr2'); bind('mvx_attr2', 'mvx_val2', 'mvx_attr1'); })(); <?php echo '</script>'; echo '</div>'; } add_action('wp_footer', 'mvx_render_variation_box', 99);
<?php
if (!defined('ABSPATH')) exit;

/* =========================
 * دسترسی
 * ========================= */
function mvx_user_can_manage() {
    return is_user_logged_in() && (
        current_user_can('manage_options') ||
        current_user_can('manage_woocommerce') ||
        current_user_can('edit_products')
    );
}

/* =========================
 * گرفتن محصول جاری
 * ========================= */
function mvx_get_current_product() {
    if (!function_exists('is_product') || !is_product()) return false;

    $product_id = get_queried_object_id();
    if (!$product_id) return false;

    $product = wc_get_product($product_id);
    return ($product && is_a($product, 'WC_Product')) ? $product : false;
}

/* =========================
 * همه attribute ها برای فرم
 * - global => value = term_id
 * - local  => value = خود متن
 * ========================= */
function mvx_get_all_attributes_for_form($product = false) {
    $result = array();

    /* global attributes */
    if (function_exists('wc_get_attribute_taxonomies')) {
        $taxonomies = wc_get_attribute_taxonomies();

        if (!empty($taxonomies)) {
            foreach ($taxonomies as $tax) {
                $taxonomy_name = wc_attribute_taxonomy_name($tax->attribute_name); // pa_color
                if (!taxonomy_exists($taxonomy_name)) continue;

                $terms = get_terms(array(
                    'taxonomy'   => $taxonomy_name,
                    'hide_empty' => false,
                ));

                $options = array();
                if (!is_wp_error($terms) && !empty($terms)) {
                    foreach ($terms as $term) {
                        $options[] = array(
                            'value'   => (string) $term->term_id,
                            'label'   => $term->name,
                            'term_id' => (int) $term->term_id,
                            'slug'    => $term->slug,
                            'name'    => $term->name,
                        );
                    }
                }

                $result[$taxonomy_name] = array(
                    'name'        => $taxonomy_name,
                    'label'       => $tax->attribute_label ? $tax->attribute_label : $tax->attribute_name,
                    'is_taxonomy' => true,
                    'options'     => $options,
                    'source'      => 'global',
                );
            }
        }
    }

    /* local product attributes */
    if ($product) {
        $product_attributes = $product->get_attributes();

        if (!empty($product_attributes)) {
            foreach ($product_attributes as $key => $attribute) {
                if (!is_a($attribute, 'WC_Product_Attribute')) continue;
                if ($attribute->is_taxonomy()) continue;

                $name  = $attribute->get_name();
                $label = wc_attribute_label($name, $product);

                $options = array();
                foreach ((array) $attribute->get_options() as $opt) {
                    $opt = (string) $opt;
                    $options[] = array(
                        'value' => $opt,
                        'label' => $opt,
                    );
                }

                $result[$name] = array(
                    'name'        => $name,
                    'label'       => $label ? $label : $name,
                    'is_taxonomy' => false,
                    'options'     => $options,
                    'source'      => 'local',
                );
            }
        }
    }

    return $result;
}

/* =========================
 * پیدا کردن تعریف attribute
 * ========================= */
function mvx_find_attribute_definition($all_attrs, $attr_name) {
    return isset($all_attrs[$attr_name]) ? $all_attrs[$attr_name] : false;
}

/* =========================
 * پیدا کردن term از روی term_id
 * ========================= */
function mvx_get_term_from_posted_value($taxonomy, $posted_value) {
    $term_id = absint($posted_value);
    if (!$term_id) return false;

    $term = get_term($term_id, $taxonomy);
    if ($term && !is_wp_error($term)) {
        return $term;
    }

    return false;
}

/* =========================
 * افزودن option به attribute محصول
 * اصلاح‌شده:
 * برای global attribute، term_id داخل options محصول هم ذخیره می‌شود
 * تا variation روی "همه موارد" نماند
 * ========================= */
function mvx_add_option_to_product_attribute($product_id, $attr_name, $posted_value, $attr_def = false) {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    $attributes = $product->get_attributes();
    $found = false;

    foreach ($attributes as $key => $attribute) {
        if (!is_a($attribute, 'WC_Product_Attribute')) continue;
        if ($attribute->get_name() !== $attr_name) continue;

        $found = true;

        if ($attribute->is_taxonomy()) {
            $term = false;

            if ($attr_def && !empty($attr_def['is_taxonomy'])) {
                $term = mvx_get_term_from_posted_value($attr_name, $posted_value);
            }

            if ($term) {
                wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true);

                $current_options = (array) $attribute->get_options();
                $current_options = array_map('intval', $current_options);

                $term_id = (int) $term->term_id;

                if (!in_array($term_id, $current_options, true)) {
                    $current_options[] = $term_id;
                }

                $attribute->set_options($current_options);
            }

            $attribute->set_id(wc_attribute_taxonomy_id_by_name($attr_name));
            $attribute->set_visible(true);
            $attribute->set_variation(true);
            $attributes[$key] = $attribute;

        } else {
            $attr_value = (string) $posted_value;
            $options = (array) $attribute->get_options();

            if (!in_array($attr_value, $options, true)) {
                $options[] = $attr_value;
                $attribute->set_options($options);
            }

            $attribute->set_visible(true);
            $attribute->set_variation(true);
            $attributes[$key] = $attribute;
        }
    }

    if (!$found) {
        $new_attr = new WC_Product_Attribute();
        $new_attr->set_name($attr_name);
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
        $new_attr->set_position(count($attributes));

        if ($attr_def && !empty($attr_def['is_taxonomy']) && taxonomy_exists($attr_name)) {
            $term = mvx_get_term_from_posted_value($attr_name, $posted_value);

            $new_attr->set_id(wc_attribute_taxonomy_id_by_name($attr_name));

            if ($term) {
                wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true);

                // نکته مهم: اینجا نباید خالی باشد
                // باید term_id داخل options ذخیره شود
                $new_attr->set_options(array((int) $term->term_id));
            } else {
                $new_attr->set_options(array());
            }
        } else {
            $attr_value = (string) $posted_value;
            $new_attr->set_options(array($attr_value));
        }

        $attributes[$attr_name] = $new_attr;
    }

    $product->set_attributes($attributes);
    $product->save();

    return true;
}

/* =========================
 * آماده‌سازی مقدار variation
 * global => slug term
 * local  => text
 * ========================= */
function mvx_prepare_variation_value($attr_def, $raw_value) {
    if (!$attr_def) {
        return array(
            'meta_value'      => $raw_value,
            'variation_value' => $raw_value,
            'display_value'   => $raw_value,
        );
    }

    if (!empty($attr_def['is_taxonomy'])) {
        $taxonomy = $attr_def['name'];
        $term = mvx_get_term_from_posted_value($taxonomy, $raw_value);

        if ($term) {
            return array(
                'meta_value'      => $term->slug,
                'variation_value' => $term->slug,
                'display_value'   => $term->name,
            );
        }

        return array(
            'meta_value'      => '',
            'variation_value' => '',
            'display_value'   => '',
        );
    }

    return array(
        'meta_value'      => (string) $raw_value,
        'variation_value' => (string) $raw_value,
        'display_value'   => (string) $raw_value,
    );
}

/* =========================
 * آیا variation وجود دارد؟
 * ========================= */
function mvx_variation_exists($product_id, $attrs_meta) {
    $children = get_posts(array(
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'post_status' => array('publish', 'private'),
        'numberposts' => -1,
        'fields'      => 'ids',
    ));

    foreach ($children as $variation_id) {
        $matched = true;

        foreach ($attrs_meta as $meta_key => $meta_value) {
            $saved = get_post_meta($variation_id, $meta_key, true);
            if ((string) $saved !== (string) $meta_value) {
                $matched = false;
                break;
            }
        }

        if ($matched) return true;
    }

    return false;
}

/* =========================
 * simple -> variable
 * ========================= */
function mvx_convert_simple_to_variable($product_id) {
    wp_set_object_terms($product_id, 'variable', 'product_type');
    wc_delete_product_transients($product_id);
    return wc_get_product($product_id);
}

/* =========================
 * پردازش فرم
 * ========================= */
function mvx_handle_form_submit() {
    if (is_admin()) return;
    if (!mvx_user_can_manage()) return;

    if (empty($_POST['mvx_action']) || $_POST['mvx_action'] !== 'add_variation_two_rows') return;

    if (empty($_POST['mvx_nonce']) || !wp_verify_nonce($_POST['mvx_nonce'], 'mvx_add_variation_two_rows')) {
        return;
    }

    $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0;

    $attr1 = isset($_POST['mvx_attr1']) ? wc_clean(wp_unslash($_POST['mvx_attr1'])) : '';
    $val1  = isset($_POST['mvx_val1']) ? wp_unslash($_POST['mvx_val1']) : '';

    $attr2 = isset($_POST['mvx_attr2']) ? wc_clean(wp_unslash($_POST['mvx_attr2'])) : '';
    $val2  = isset($_POST['mvx_val2']) ? wp_unslash($_POST['mvx_val2']) : '';

    $price = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : '';

    if (!$product_id || !$attr1 || $val1 === '') {
        wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id)));
        exit;
    }

    if ($attr1 && $attr2 && $attr1 === $attr2) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'sameattr', get_permalink($product_id)));
        exit;
    }

    $product = wc_get_product($product_id);
    if (!$product) return;

    if ($product->get_type() === 'simple') {
        $product = mvx_convert_simple_to_variable($product_id);
    }

    if (!$product || $product->get_type() !== 'variable') {
        wp_safe_redirect(add_query_arg('mvx_msg', 'notvariable', get_permalink($product_id)));
        exit;
    }

    $all_attrs = mvx_get_all_attributes_for_form($product);

    $def1 = mvx_find_attribute_definition($all_attrs, $attr1);
    $def2 = $attr2 ? mvx_find_attribute_definition($all_attrs, $attr2) : false;

    if (!$def1) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id)));
        exit;
    }

    if ($attr2 && !$def2) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id)));
        exit;
    }

    mvx_add_option_to_product_attribute($product_id, $attr1, $val1, $def1);

    if ($attr2 && $val2 !== '') {
        mvx_add_option_to_product_attribute($product_id, $attr2, $val2, $def2);
    }

    // بعد از ذخیره attributeهای محصول، محصول را تازه می‌کنیم
    $product = wc_get_product($product_id);

    $prepared1 = mvx_prepare_variation_value($def1, $val1);

    if ($prepared1['meta_value'] === '') {
        wp_safe_redirect(add_query_arg('mvx_msg', 'badvalue', get_permalink($product_id)));
        exit;
    }

    $variation_meta = array(
        'attribute_' . $attr1 => $prepared1['meta_value'],
    );

    $variation_set_attrs = array(
        $attr1 => $prepared1['variation_value'],
    );

    if ($attr2 && $val2 !== '') {
        $prepared2 = mvx_prepare_variation_value($def2, $val2);

        if ($prepared2['meta_value'] === '') {
            wp_safe_redirect(add_query_arg('mvx_msg', 'badvalue', get_permalink($product_id)));
            exit;
        }

        $variation_meta['attribute_' . $attr2] = $prepared2['meta_value'];
        $variation_set_attrs[$attr2] = $prepared2['variation_value'];
    }

    if (mvx_variation_exists($product_id, $variation_meta)) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'exists', get_permalink($product_id)));
        exit;
    }

    $variation = new WC_Product_Variation();
    $variation->set_parent_id($product_id);
    $variation->set_status('publish');
    $variation->set_attributes($variation_set_attrs);

    if ($price !== '') {
        $variation->set_regular_price($price);
        $variation->set_price($price);
    }

    $variation_id = $variation->save();

    if (!$variation_id || is_wp_error($variation_id)) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'error', get_permalink($product_id)));
        exit;
    }

    foreach ($variation_meta as $meta_key => $meta_value) {
        update_post_meta($variation_id, $meta_key, $meta_value);
    }

    update_post_meta($variation_id, '_virtual', 'no');
    update_post_meta($variation_id, '_downloadable', 'no');

    WC_Product_Variable::sync($product_id);
    wc_delete_product_transients($product_id);

    wp_safe_redirect(add_query_arg(array(
        'mvx_msg' => 'created',
        'mvx_vid' => $variation_id,
    ), get_permalink($product_id)));
    exit;
}
add_action('template_redirect', 'mvx_handle_form_submit');

/* =========================
 * UI
 * ========================= */
function mvx_render_variation_box() {
    if (is_admin()) return;
    if (!function_exists('is_product') || !is_product()) return;
    if (!mvx_user_can_manage()) return;

    $product = mvx_get_current_product();
    if (!$product) return;

    $attrs_assoc = mvx_get_all_attributes_for_form($product);
    $attrs = array_values($attrs_assoc);

    echo '<div id="mvx-box" style="position:fixed;left:20px;bottom:20px;width:560px;max-width:calc(100vw - 30px);z-index:999999;background:#fff;border:2px solid #2563eb;border-radius:14px;box-shadow:0 12px 35px rgba(0,0,0,.18);padding:14px;direction:rtl;text-align:right;">';

    echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">';
    echo '<strong style="font-size:15px;">افزودن تنوع</strong>';
    echo '<button type="button" onclick="document.getElementById(\'mvx-box\').style.display=\'none\';" style="background:#e5e7eb;border:none;border-radius:8px;padding:2px 8px;cursor:pointer;">×</button>';
    echo '</div>';

    if (isset($_GET['mvx_msg'])) {
        $msg = sanitize_text_field(wp_unslash($_GET['mvx_msg']));
        $style_ok  = 'background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;padding:8px 10px;border-radius:10px;margin-bottom:10px;';
        $style_bad = 'background:#fef2f2;color:#991b1b;border:1px solid #fecaca;padding:8px 10px;border-radius:10px;margin-bottom:10px;';
        $style_wrn = 'background:#fff7ed;color:#9a3412;border:1px solid #fdba74;padding:8px 10px;border-radius:10px;margin-bottom:10px;';

        if ($msg === 'created') {
            echo '<div style="'.$style_ok.'">تنوع ساخته شد.</div>';
        } elseif ($msg === 'exists') {
            echo '<div style="'.$style_wrn.'">این ترکیب از قبل وجود دارد.</div>';
        } elseif ($msg === 'sameattr') {
            echo '<div style="'.$style_bad.'">ویژگی ردیف اول و دوم نباید یکی باشد.</div>';
        } elseif ($msg === 'missing') {
            echo '<div style="'.$style_bad.'">ویژگی و مقدار معتبر الزامی است.</div>';
        } elseif ($msg === 'notvariable') {
            echo '<div style="'.$style_bad.'">محصول به variable تبدیل نشد.</div>';
        } elseif ($msg === 'badvalue') {
            echo '<div style="'.$style_bad.'">مقدار انتخابی معتبر نیست.</div>';
        } elseif ($msg === 'error') {
            echo '<div style="'.$style_bad.'">خطا در ساخت variation.</div>';
        }
    }

    echo '<form method="post">';
    echo wp_nonce_field('mvx_add_variation_two_rows', 'mvx_nonce', true, false);
    echo '<input type="hidden" name="mvx_action" value="add_variation_two_rows">';
    echo '<input type="hidden" name="mvx_product_id" value="' . esc_attr($product->get_id()) . '">';

    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;">';
    echo '<div>';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">ویژگی</label>';
    echo '<select id="mvx_attr1" name="mvx_attr1" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '<option value="">انتخاب ویژگی</option>';
    foreach ($attrs as $a) {
        echo '<option value="' . esc_attr($a['name']) . '">' . esc_html($a['label']) . '</option>';
    }
    echo '</select>';
    echo '</div>';

    echo '<div>';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">مقدار</label>';
    echo '<select id="mvx_val1" name="mvx_val1" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '<option value="">ابتدا ویژگی را انتخاب کنید</option>';
    echo '</select>';
    echo '</div>';
    echo '</div>';

    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;">';
    echo '<div>';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">ویژگی</label>';
    echo '<select id="mvx_attr2" name="mvx_attr2" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '<option value="">انتخاب ویژگی</option>';
    foreach ($attrs as $a) {
        echo '<option value="' . esc_attr($a['name']) . '">' . esc_html($a['label']) . '</option>';
    }
    echo '</select>';
    echo '</div>';

    echo '<div>';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">مقدار</label>';
    echo '<select id="mvx_val2" name="mvx_val2" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '<option value="">ابتدا ویژگی را انتخاب کنید</option>';
    echo '</select>';
    echo '</div>';
    echo '</div>';

    echo '<div style="margin-bottom:12px;">';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">قیمت</label>';
    echo '<input type="number" step="any" name="mvx_price" placeholder="مثلاً 350000" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '</div>';

    echo '<button type="submit" onclick="return confirm(\'تنوع اضافه شود؟\');" style="width:100%;background:#2563eb;color:#fff;border:none;border-radius:10px;padding:12px;cursor:pointer;font-weight:700;">افزودن تنوع</button>';
    echo '</form>';

    $map = array();
    foreach ($attrs as $a) {
        $map[$a['name']] = $a['options'];
    }

    echo '<script>';
    echo 'window.mvxAttrMap = ' . wp_json_encode($map) . ';';
    ?>
    (function(){
        function bind(attrId, valId, otherAttrId){
            var attr = document.getElementById(attrId);
            var val  = document.getElementById(valId);
            var other = document.getElementById(otherAttrId);
            if(!attr || !val) return;

            function refill(){
                val.innerHTML = '';
                var selected = attr.value;

                if(other && other.value && selected && other.value === selected){
                    alert('این ویژگی در ردیف دیگر انتخاب شده است.');
                    attr.value = '';
                    selected = '';
                }

                if(!selected){
                    var op = document.createElement('option');
                    op.value = '';
                    op.textContent = 'ابتدا ویژگی را انتخاب کنید';
                    val.appendChild(op);
                    return;
                }

                var items = (window.mvxAttrMap && window.mvxAttrMap[selected]) ? window.mvxAttrMap[selected] : [];

                if(!items.length){
                    var op2 = document.createElement('option');
                    op2.value = '';
                    op2.textContent = 'مقداری یافت نشد';
                    val.appendChild(op2);
                    return;
                }

                var first = document.createElement('option');
                first.value = '';
                first.textContent = 'انتخاب مقدار';
                val.appendChild(first);

                items.forEach(function(item){
                    var op3 = document.createElement('option');
                    op3.value = item.value;
                    op3.textContent = item.label;
                    val.appendChild(op3);
                });
            }

            attr.addEventListener('change', refill);
            refill();
        }

        bind('mvx_attr1', 'mvx_val1', 'mvx_attr2');
        bind('mvx_attr2', 'mvx_val2', 'mvx_attr1');
    })();
    <?php
    echo '</script>';

    echo '</div>';
}
add_action('wp_footer', 'mvx_render_variation_box', 99);
کارگری
TEXT - 2026-05-05 22:21:22
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span> </div> </div> <div class="summary-grid"> <div class="summary-card dark"> <small>تعداد کل امروز</small> <strong id="regTotalQty">۰</strong> </div> <div class="summary-card green"> <small>جمع مبلغ امروز</small> <strong id="regTotalPrice">۰ تومان</strong> </div> </div> <div class="search-box"> <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..."> </div> <div class="section-title">خدمات پرکاربرد</div> <div class="chips" id="serviceChips"></div> <div id="selectedServiceBox" class="selected-box"> <div class="empty-box">یک خدمت را انتخاب کن</div> </div> <div class="section-title">ثبت‌های امروز</div> <div class="list-box" id="todayItems"> <div class="empty-list">هنوز چیزی ثبت نشده</div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> <!-- آمار --> <section class="page" id="page-stats"> <div class="page-title"> <div> <h2>آمار</h2> <span>گزارش روزانه، هفتگی، ماهانه و کلی</span> </div> </div> <div class="stats-top-grid"> <div class="stats-card navy"> <small>امروز</small> <strong id="statsTodayAmount">۰ تومان</strong> <span id="statsTodayCount">۰ ثبت</span> </div> <div class="stats-card emerald"> <small>این هفته</small> <strong id="statsWeekAmount">۰ تومان</strong> <span id="statsWeekCount">۰ ثبت</span> </div> <div class="stats-card violet"> <small>این ماه</small> <strong id="statsMonthAmount">۰ تومان</strong> <span id="statsMonthCount">۰ ثبت</span> </div> <div class="stats-card orange"> <small>جمع کل</small> <strong id="statsAllAmount">۰ تومان</strong> <span id="statsAllCount">۰ ثبت</span> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>روزهای کار شده</small> <strong id="workedDaysCount">۰ روز</strong> </div> <div class="mini-card success"> <small>میانگین روزانه</small> <strong id="avgDailyAmount">۰ تومان</strong> </div> </div> <div class="section-title">نمودار مبلغ روزها</div> <div class="chart-card"> <div class="bars-chart" id="amountChart"></div> </div> <div class="section-title">روزهای کار شده</div> <div class="chart-card"> <div class="days-strip" id="workedDaysStrip"></div> </div> <div class="section-title">خلاصه روزها</div> <div class="list-box" id="dailyStatsList"> <div class="empty-list">آماری وجود ندارد</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav five"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> <button class="tab-btn" data-page="stats">آمار</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 110px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small,.stats-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .stats-top-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .stats-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .stats-card span{ display:block; margin-top:8px; font-size:12px; opacity:.92; } .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); } .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); } .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); } .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); } .chart-card{ background:#fff; border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.05); margin-bottom:14px; } .bars-chart{ height:220px; display:flex; align-items:flex-end; gap:10px; overflow-x:auto; padding-top:10px; } .bar-item{ min-width:46px; display:flex; flex-direction:column; align-items:center; gap:8px; } .bar{ width:100%; border-radius:14px 14px 6px 6px; background:linear-gradient(180deg,#60a5fa,#2563eb); min-height:10px; position:relative; } .bar-value{ font-size:10px; color:#334155; font-weight:700; text-align:center; line-height:1.4; } .bar-label{ font-size:11px; color:#64748b; font-weight:700; } .days-strip{ display:flex; flex-wrap:wrap; gap:10px; } .day-pill{ padding:10px 12px; border-radius:999px; background:#e0f2fe; color:#075985; font-size:12px; font-weight:800; } .day-pill.off{ background:#f1f5f9; color:#94a3b8; } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .bottom-nav.five{ grid-template-columns:repeat(5,1fr); } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:11px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:90px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } @media (max-width:390px){ .factory-phone{ padding:16px 12px 112px; } .mini-grid.three{ grid-template-columns:1fr; } .stats-top-grid{ grid-template-columns:1fr 1fr; } .tab-btn{ font-size:10px; } } </style> <script> (function(){ const services = [ { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 }, { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 }, { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 }, { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 }, { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 }, { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 } ]; let selectedService = null; let currentQty = 1; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" }, { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" }, { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" }, { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" }, { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" }, { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" }, { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const chipsBox = document.getElementById("serviceChips"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const todayItems = document.getElementById("todayItems"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const serviceSearch = document.getElementById("serviceSearch"); const toast = document.getElementById("toast"); const todayStr = "2026-05-05"; function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getAmount(item){ return item.qty * item.price; } function isSameDate(date1, date2){ return date1 === date2; } function getDateObj(str){ return new Date(str + "T00:00:00"); } function diffDays(from, to){ const ms = getDateObj(to) - getDateObj(from); return Math.floor(ms / (1000 * 60 * 60 * 24)); } function getFilteredServices(){ const q = normalizeText(serviceSearch.value.trim()); if(!q) return services; return services.filter(s => normalizeText(s.name).includes(q)); } function renderChips(list = services){ chipsBox.innerHTML = ""; list.forEach(service => { const btn = document.createElement("button"); btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : ""); btn.textContent = service.name; btn.onclick = function(){ selectedService = service; currentQty = 1; renderChips(getFilteredServices()); renderSelectedService(); }; chipsBox.appendChild(btn); }); } function renderSelectedService(){ if(!selectedService){ selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`; return; } selectedServiceBox.innerHTML = ` <div class="service-card"> <div class="service-card-top"> <div> <h3>${selectedService.name}</h3> <p>قیمت واحد: ${money(selectedService.price)}</p> </div> <div class="price-badge">${money(selectedService.price * currentQty)}</div> </div> <div class="counter"> <button type="button" id="minusQty">−</button> <input type="number" id="qtyInput" min="1" value="${currentQty}"> <button type="button" id="plusQty">+</button> </div> <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button> </div> `; document.getElementById("minusQty").onclick = function(){ currentQty = Math.max(1, currentQty - 1); renderSelectedService(); }; document.getElementById("plusQty").onclick = function(){ currentQty++; renderSelectedService(); }; document.getElementById("qtyInput").oninput = function(e){ currentQty = Math.max(1, parseInt(e.target.value || "1")); renderSelectedService(); }; document.getElementById("addTodayBtn").onclick = function(){ entries.unshift({ id: Date.now(), serviceId: selectedService.id, name: selectedService.name, price: selectedService.price, qty: currentQty, status: "pending", worker: "عرفان", date: todayStr }); currentQty = 1; renderAll(); showToast("ثبت جدید اضافه شد"); }; } function renderTodayItems(){ const todayEntries = entries.filter(item => item.date === todayStr); if(todayEntries.length === 0){ todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`; return; } todayItems.innerHTML = ""; todayEntries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> `; row.querySelector(".btn-remove").onclick = function(){ entries = entries.filter(e => e.id !== item.id); renderAll(); showToast("آیتم حذف شد"); }; todayItems.appendChild(row); }); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> تاریخ: ${toFa(item.date)} <br> ${toFa(item.qty)} عدد | ${money(getAmount(item))} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += getAmount(item); }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderRegisterSummary(){ const todayEntries = entries.filter(item => item.date === todayStr); const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0); const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0); document.getElementById("regTotalQty").textContent = toFa(totalQty); document.getElementById("regTotalPrice").textContent = money(totalPrice); } function renderStatsPage(){ const todayEntries = entries.filter(item => isSameDate(item.date, todayStr)); const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7); const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7)); const allEntries = entries; const todayAmount = todayEntries.reduce((s,i)=>s+getAmount(i),0); const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0); const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0); const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0); document.getElementById("statsTodayAmount").textContent = money(todayAmount); document.getElementById("statsWeekAmount").textContent = money(weekAmount); document.getElementById("statsMonthAmount").textContent = money(monthAmount); document.getElementById("statsAllAmount").textContent = money(allAmount); document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت"; document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت"; document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت"; document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت"; const uniqueDays = [...new Set(entries.map(i => i.date))].sort(); document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز"; const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0; document.getElementById("avgDailyAmount").textContent = money(avg); const dayMap = {}; entries.forEach(item => { if(!dayMap[item.date]){ dayMap[item.date] = { amount: 0, qty: 0, count: 0 }; } dayMap[item.date].amount += getAmount(item); dayMap[item.date].qty += item.qty; dayMap[item.date].count += 1; }); const sortedDays = Object.keys(dayMap).sort(); const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1); const amountChart = document.getElementById("amountChart"); amountChart.innerHTML = ""; sortedDays.forEach(day => { const amount = dayMap[day].amount; const height = Math.max(12, Math.round((amount / maxAmount) * 160)); const dayLabel = day.slice(5).replace("-", "/"); const item = document.createElement("div"); item.className = "bar-item"; item.innerHTML = ` <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div> <div class="bar" style="height:${height}px"></div> <div class="bar-label">${toFa(dayLabel)}</div> `; amountChart.appendChild(item); }); const workedDaysStrip = document.getElementById("workedDaysStrip"); workedDaysStrip.innerHTML = ""; if(sortedDays.length === 0){ workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`; } else { sortedDays.forEach(day => { const pill = document.createElement("div"); pill.className = "day-pill"; pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/")); workedDaysStrip.appendChild(pill); }); } const dailyStatsList = document.getElementById("dailyStatsList"); if(sortedDays.length === 0){ dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`; } else { dailyStatsList.innerHTML = ""; [...sortedDays].reverse().forEach(day => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>تاریخ ${toFa(day)}</h4> <p> تعداد ثبت: ${toFa(dayMap[day].count)} <br> تعداد تولید: ${toFa(dayMap[day].qty)} عدد <br> مبلغ روز: ${money(dayMap[day].amount)} </p> </div> <div></div> `; dailyStatsList.appendChild(row); }); } } function renderAll(){ renderChips(getFilteredServices()); renderSelectedService(); renderTodayItems(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderRegisterSummary(); renderStatsPage(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function(){ renderChips(getFilteredServices()); }); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
          </div>
        </div>

        <div class="summary-grid">
          <div class="summary-card dark">
            <small>تعداد کل امروز</small>
            <strong id="regTotalQty">۰</strong>
          </div>
          <div class="summary-card green">
            <small>جمع مبلغ امروز</small>
            <strong id="regTotalPrice">۰ تومان</strong>
          </div>
        </div>

        <div class="search-box">
          <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت...">
        </div>

        <div class="section-title">خدمات پرکاربرد</div>
        <div class="chips" id="serviceChips"></div>

        <div id="selectedServiceBox" class="selected-box">
          <div class="empty-box">یک خدمت را انتخاب کن</div>
        </div>

        <div class="section-title">ثبت‌های امروز</div>
        <div class="list-box" id="todayItems">
          <div class="empty-list">هنوز چیزی ثبت نشده</div>
        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>

      <!-- آمار -->
      <section class="page" id="page-stats">
        <div class="page-title">
          <div>
            <h2>آمار</h2>
            <span>گزارش روزانه، هفتگی، ماهانه و کلی</span>
          </div>
        </div>

        <div class="stats-top-grid">
          <div class="stats-card navy">
            <small>امروز</small>
            <strong id="statsTodayAmount">۰ تومان</strong>
            <span id="statsTodayCount">۰ ثبت</span>
          </div>
          <div class="stats-card emerald">
            <small>این هفته</small>
            <strong id="statsWeekAmount">۰ تومان</strong>
            <span id="statsWeekCount">۰ ثبت</span>
          </div>
          <div class="stats-card violet">
            <small>این ماه</small>
            <strong id="statsMonthAmount">۰ تومان</strong>
            <span id="statsMonthCount">۰ ثبت</span>
          </div>
          <div class="stats-card orange">
            <small>جمع کل</small>
            <strong id="statsAllAmount">۰ تومان</strong>
            <span id="statsAllCount">۰ ثبت</span>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>روزهای کار شده</small>
            <strong id="workedDaysCount">۰ روز</strong>
          </div>
          <div class="mini-card success">
            <small>میانگین روزانه</small>
            <strong id="avgDailyAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">نمودار مبلغ روزها</div>
        <div class="chart-card">
          <div class="bars-chart" id="amountChart"></div>
        </div>

        <div class="section-title">روزهای کار شده</div>
        <div class="chart-card">
          <div class="days-strip" id="workedDaysStrip"></div>
        </div>

        <div class="section-title">خلاصه روزها</div>
        <div class="list-box" id="dailyStatsList">
          <div class="empty-list">آماری وجود ندارد</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav five">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
      <button class="tab-btn" data-page="stats">آمار</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 110px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{ display:none; }
  .page.active{ display:block; }

  .page-title{ margin-bottom:16px; }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }

  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); }
  .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); }

  .search-box{ margin-bottom:14px; }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }

  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }

  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{ display:none; }

  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }

  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }

  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }

  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }

  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }

  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }

  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }

  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }

  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }

  .wallet-card small,.mini-card small,.manager-card small,.stats-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong,.stats-card strong{
    font-size:17px;
    font-weight:800;
  }

  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .stats-top-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .stats-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .stats-card span{
    display:block;
    margin-top:8px;
    font-size:12px;
    opacity:.92;
  }
  .stats-card.navy{ background:linear-gradient(135deg,#0f172a,#1e3a8a); }
  .stats-card.emerald{ background:linear-gradient(135deg,#059669,#10b981); }
  .stats-card.violet{ background:linear-gradient(135deg,#7c3aed,#a855f7); }
  .stats-card.orange{ background:linear-gradient(135deg,#ea580c,#f59e0b); }

  .chart-card{
    background:#fff;
    border-radius:22px;
    padding:14px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    margin-bottom:14px;
  }

  .bars-chart{
    height:220px;
    display:flex;
    align-items:flex-end;
    gap:10px;
    overflow-x:auto;
    padding-top:10px;
  }
  .bar-item{
    min-width:46px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
  }
  .bar{
    width:100%;
    border-radius:14px 14px 6px 6px;
    background:linear-gradient(180deg,#60a5fa,#2563eb);
    min-height:10px;
    position:relative;
  }
  .bar-value{
    font-size:10px;
    color:#334155;
    font-weight:700;
    text-align:center;
    line-height:1.4;
  }
  .bar-label{
    font-size:11px;
    color:#64748b;
    font-weight:700;
  }

  .days-strip{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .day-pill{
    padding:10px 12px;
    border-radius:999px;
    background:#e0f2fe;
    color:#075985;
    font-size:12px;
    font-weight:800;
  }
  .day-pill.off{
    background:#f1f5f9;
    color:#94a3b8;
  }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .bottom-nav.five{
    grid-template-columns:repeat(5,1fr);
  }

  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:11px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:90px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  @media (max-width:390px){
    .factory-phone{ padding:16px 12px 112px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .stats-top-grid{ grid-template-columns:1fr 1fr; }
    .tab-btn{ font-size:10px; }
  }
</style>

<script>
(function(){
  const services = [
    { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 },
    { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 },
    { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 },
    { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 },
    { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 },
    { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 }
  ];

  let selectedService = null;
  let currentQty = 1;

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان", date: "2026-05-05" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان", date: "2026-05-05" },
    { id: 1003, serviceId: 2, name: "میز لبه‌دار ۴۲", price: 102000, qty: 3, status: "approved", worker: "عرفان", date: "2026-05-04" },
    { id: 1004, serviceId: 4, name: "میز لبه‌دار ۶۰", price: 125000, qty: 2, status: "approved", worker: "عرفان", date: "2026-05-03" },
    { id: 1005, serviceId: 5, name: "میز لبه‌دار ۷۰", price: 140000, qty: 1, status: "pending", worker: "عرفان", date: "2026-05-02" },
    { id: 1006, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 4, status: "approved", worker: "عرفان", date: "2026-05-01" },
    { id: 1007, serviceId: 6, name: "میز لبه‌دار ۸۰", price: 155000, qty: 2, status: "approved", worker: "عرفان", date: "2026-04-28" },
    { id: 1008, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "rejected", worker: "عرفان", date: "2026-04-25" }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const chipsBox = document.getElementById("serviceChips");
  const selectedServiceBox = document.getElementById("selectedServiceBox");
  const todayItems = document.getElementById("todayItems");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const serviceSearch = document.getElementById("serviceSearch");
  const toast = document.getElementById("toast");

  const todayStr = "2026-05-05";

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getAmount(item){
    return item.qty * item.price;
  }

  function isSameDate(date1, date2){
    return date1 === date2;
  }

  function getDateObj(str){
    return new Date(str + "T00:00:00");
  }

  function diffDays(from, to){
    const ms = getDateObj(to) - getDateObj(from);
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  function getFilteredServices(){
    const q = normalizeText(serviceSearch.value.trim());
    if(!q) return services;
    return services.filter(s => normalizeText(s.name).includes(q));
  }

  function renderChips(list = services){
    chipsBox.innerHTML = "";
    list.forEach(service => {
      const btn = document.createElement("button");
      btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : "");
      btn.textContent = service.name;
      btn.onclick = function(){
        selectedService = service;
        currentQty = 1;
        renderChips(getFilteredServices());
        renderSelectedService();
      };
      chipsBox.appendChild(btn);
    });
  }

  function renderSelectedService(){
    if(!selectedService){
      selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`;
      return;
    }

    selectedServiceBox.innerHTML = `
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>${selectedService.name}</h3>
            <p>قیمت واحد: ${money(selectedService.price)}</p>
          </div>
          <div class="price-badge">${money(selectedService.price * currentQty)}</div>
        </div>

        <div class="counter">
          <button type="button" id="minusQty">−</button>
          <input type="number" id="qtyInput" min="1" value="${currentQty}">
          <button type="button" id="plusQty">+</button>
        </div>

        <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button>
      </div>
    `;

    document.getElementById("minusQty").onclick = function(){
      currentQty = Math.max(1, currentQty - 1);
      renderSelectedService();
    };

    document.getElementById("plusQty").onclick = function(){
      currentQty++;
      renderSelectedService();
    };

    document.getElementById("qtyInput").oninput = function(e){
      currentQty = Math.max(1, parseInt(e.target.value || "1"));
      renderSelectedService();
    };

    document.getElementById("addTodayBtn").onclick = function(){
      entries.unshift({
        id: Date.now(),
        serviceId: selectedService.id,
        name: selectedService.name,
        price: selectedService.price,
        qty: currentQty,
        status: "pending",
        worker: "عرفان",
        date: todayStr
      });
      currentQty = 1;
      renderAll();
      showToast("ثبت جدید اضافه شد");
    };
  }

  function renderTodayItems(){
    const todayEntries = entries.filter(item => item.date === todayStr);

    if(todayEntries.length === 0){
      todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`;
      return;
    }

    todayItems.innerHTML = "";
    todayEntries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      `;
      row.querySelector(".btn-remove").onclick = function(){
        entries = entries.filter(e => e.id !== item.id);
        renderAll();
        showToast("آیتم حذف شد");
      };
      todayItems.appendChild(row);
    });
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تاریخ: ${toFa(item.date)}
              <br>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(getAmount(item))}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + getAmount(item), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            تاریخ: ${toFa(item.date)}
            <br>
            ${toFa(item.qty)} عدد | ${money(getAmount(item))}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + getAmount(item), 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += getAmount(item);
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderRegisterSummary(){
    const todayEntries = entries.filter(item => item.date === todayStr);
    const totalQty = todayEntries.reduce((sum, item) => sum + item.qty, 0);
    const totalPrice = todayEntries.reduce((sum, item) => sum + getAmount(item), 0);

    document.getElementById("regTotalQty").textContent = toFa(totalQty);
    document.getElementById("regTotalPrice").textContent = money(totalPrice);
  }

  function renderStatsPage(){
    const todayEntries = entries.filter(item => isSameDate(item.date, todayStr));
    const weekEntries = entries.filter(item => diffDays(item.date, todayStr) >= 0 && diffDays(item.date, todayStr) < 7);
    const monthEntries = entries.filter(item => item.date.slice(0,7) === todayStr.slice(0,7));
    const allEntries = entries;

    const todayAmount = todayEntries.reduce((s,i)=>s+getAmount(i),0);
    const weekAmount = weekEntries.reduce((s,i)=>s+getAmount(i),0);
    const monthAmount = monthEntries.reduce((s,i)=>s+getAmount(i),0);
    const allAmount = allEntries.reduce((s,i)=>s+getAmount(i),0);

    document.getElementById("statsTodayAmount").textContent = money(todayAmount);
    document.getElementById("statsWeekAmount").textContent = money(weekAmount);
    document.getElementById("statsMonthAmount").textContent = money(monthAmount);
    document.getElementById("statsAllAmount").textContent = money(allAmount);

    document.getElementById("statsTodayCount").textContent = toFa(todayEntries.length) + " ثبت";
    document.getElementById("statsWeekCount").textContent = toFa(weekEntries.length) + " ثبت";
    document.getElementById("statsMonthCount").textContent = toFa(monthEntries.length) + " ثبت";
    document.getElementById("statsAllCount").textContent = toFa(allEntries.length) + " ثبت";

    const uniqueDays = [...new Set(entries.map(i => i.date))].sort();
    document.getElementById("workedDaysCount").textContent = toFa(uniqueDays.length) + " روز";

    const avg = uniqueDays.length ? Math.round(allAmount / uniqueDays.length) : 0;
    document.getElementById("avgDailyAmount").textContent = money(avg);

    const dayMap = {};
    entries.forEach(item => {
      if(!dayMap[item.date]){
        dayMap[item.date] = { amount: 0, qty: 0, count: 0 };
      }
      dayMap[item.date].amount += getAmount(item);
      dayMap[item.date].qty += item.qty;
      dayMap[item.date].count += 1;
    });

    const sortedDays = Object.keys(dayMap).sort();
    const maxAmount = Math.max(...sortedDays.map(day => dayMap[day].amount), 1);

    const amountChart = document.getElementById("amountChart");
    amountChart.innerHTML = "";
    sortedDays.forEach(day => {
      const amount = dayMap[day].amount;
      const height = Math.max(12, Math.round((amount / maxAmount) * 160));
      const dayLabel = day.slice(5).replace("-", "/");

      const item = document.createElement("div");
      item.className = "bar-item";
      item.innerHTML = `
        <div class="bar-value">${toFa(Math.round(amount/1000))}هزار</div>
        <div class="bar" style="height:${height}px"></div>
        <div class="bar-label">${toFa(dayLabel)}</div>
      `;
      amountChart.appendChild(item);
    });

    const workedDaysStrip = document.getElementById("workedDaysStrip");
    workedDaysStrip.innerHTML = "";
    if(sortedDays.length === 0){
      workedDaysStrip.innerHTML = `<div class="empty-list" style="width:100%">روز کاری ثبت نشده</div>`;
    } else {
      sortedDays.forEach(day => {
        const pill = document.createElement("div");
        pill.className = "day-pill";
        pill.textContent = "روز " + toFa(day.slice(5).replace("-", "/"));
        workedDaysStrip.appendChild(pill);
      });
    }

    const dailyStatsList = document.getElementById("dailyStatsList");
    if(sortedDays.length === 0){
      dailyStatsList.innerHTML = `<div class="empty-list">آماری وجود ندارد</div>`;
    } else {
      dailyStatsList.innerHTML = "";
      [...sortedDays].reverse().forEach(day => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>تاریخ ${toFa(day)}</h4>
            <p>
              تعداد ثبت: ${toFa(dayMap[day].count)}
              <br>
              تعداد تولید: ${toFa(dayMap[day].qty)} عدد
              <br>
              مبلغ روز: ${money(dayMap[day].amount)}
            </p>
          </div>
          <div></div>
        `;
        dailyStatsList.appendChild(row);
      });
    }
  }

  function renderAll(){
    renderChips(getFilteredServices());
    renderSelectedService();
    renderTodayItems();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderRegisterSummary();
    renderStatsPage();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function(){
    renderChips(getFilteredServices());
  });

  renderAll();
})();
</script>
برنامه مارگر ۲۴
TEXT - 2026-05-05 22:05:53
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span> </div> </div> <div class="summary-grid"> <div class="summary-card dark"> <small>تعداد کل امروز</small> <strong id="regTotalQty">۰</strong> </div> <div class="summary-card green"> <small>جمع مبلغ امروز</small> <strong id="regTotalPrice">۰ تومان</strong> </div> </div> <div class="search-box"> <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..."> </div> <div class="section-title">خدمات پرکاربرد</div> <div class="chips" id="serviceChips"></div> <div id="selectedServiceBox" class="selected-box"> <div class="empty-box">یک خدمت را انتخاب کن</div> </div> <div class="section-title">ثبت‌های امروز</div> <div class="list-box" id="todayItems"> <div class="empty-list">هنوز چیزی ثبت نشده</div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 95px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; grid-template-columns:repeat(4,1fr); gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:12px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:86px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } @media (max-width:360px){ .factory-phone{ padding:16px 12px 95px; } .mini-grid.three{ grid-template-columns:1fr; } .manager-grid{ grid-template-columns:1fr 1fr; } } </style> <script> (function(){ const services = [ { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 }, { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 }, { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 }, { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 }, { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 }, { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 } ]; let selectedService = null; let currentQty = 1; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان" } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const chipsBox = document.getElementById("serviceChips"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const todayItems = document.getElementById("todayItems"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const serviceSearch = document.getElementById("serviceSearch"); const toast = document.getElementById("toast"); function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getFilteredServices(){ const q = normalizeText(serviceSearch.value.trim()); if(!q) return services; return services.filter(s => normalizeText(s.name).includes(q)); } function renderChips(list = services){ chipsBox.innerHTML = ""; list.forEach(service => { const btn = document.createElement("button"); btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : ""); btn.textContent = service.name; btn.onclick = function(){ selectedService = service; currentQty = 1; renderChips(getFilteredServices()); renderSelectedService(); }; chipsBox.appendChild(btn); }); } function renderSelectedService(){ if(!selectedService){ selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`; return; } selectedServiceBox.innerHTML = ` <div class="service-card"> <div class="service-card-top"> <div> <h3>${selectedService.name}</h3> <p>قیمت واحد: ${money(selectedService.price)}</p> </div> <div class="price-badge">${money(selectedService.price * currentQty)}</div> </div> <div class="counter"> <button type="button" id="minusQty">−</button> <input type="number" id="qtyInput" min="1" value="${currentQty}"> <button type="button" id="plusQty">+</button> </div> <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button> </div> `; document.getElementById("minusQty").onclick = function(){ currentQty = Math.max(1, currentQty - 1); renderSelectedService(); }; document.getElementById("plusQty").onclick = function(){ currentQty++; renderSelectedService(); }; document.getElementById("qtyInput").oninput = function(e){ currentQty = Math.max(1, parseInt(e.target.value || "1")); renderSelectedService(); }; document.getElementById("addTodayBtn").onclick = function(){ entries.unshift({ id: Date.now(), serviceId: selectedService.id, name: selectedService.name, price: selectedService.price, qty: currentQty, status: "pending", worker: "عرفان" }); currentQty = 1; renderAll(); showToast("ثبت جدید اضافه شد"); }; } function renderTodayItems(){ if(entries.length === 0){ todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`; return; } todayItems.innerHTML = ""; entries.forEach((item, index) => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(item.qty * item.price)} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> `; row.querySelector(".btn-remove").onclick = function(){ entries.splice(index, 1); renderAll(); showToast("آیتم حذف شد"); }; todayItems.appendChild(row); }); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(item.qty * item.price)} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + (item.qty * item.price), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + (item.qty * item.price), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + (item.qty * item.price), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> ${toFa(item.qty)} عدد | ${money(item.qty * item.price)} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + item.qty * item.price, 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += item.qty * item.price; }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderRegisterSummary(){ const totalQty = entries.reduce((sum, item) => sum + item.qty, 0); const totalPrice = entries.reduce((sum, item) => sum + item.qty * item.price, 0); document.getElementById("regTotalQty").textContent = toFa(totalQty); document.getElementById("regTotalPrice").textContent = money(totalPrice); } function renderAll(){ renderChips(getFilteredServices()); renderSelectedService(); renderTodayItems(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderRegisterSummary(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function(){ renderChips(getFilteredServices()); }); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
          </div>
        </div>

        <div class="summary-grid">
          <div class="summary-card dark">
            <small>تعداد کل امروز</small>
            <strong id="regTotalQty">۰</strong>
          </div>
          <div class="summary-card green">
            <small>جمع مبلغ امروز</small>
            <strong id="regTotalPrice">۰ تومان</strong>
          </div>
        </div>

        <div class="search-box">
          <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت...">
        </div>

        <div class="section-title">خدمات پرکاربرد</div>
        <div class="chips" id="serviceChips"></div>

        <div id="selectedServiceBox" class="selected-box">
          <div class="empty-box">یک خدمت را انتخاب کن</div>
        </div>

        <div class="section-title">ثبت‌های امروز</div>
        <div class="list-box" id="todayItems">
          <div class="empty-list">هنوز چیزی ثبت نشده</div>
        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 95px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{
    display:none;
  }
  .page.active{
    display:block;
  }
  .page-title{
    margin-bottom:16px;
  }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }
  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{
    background:linear-gradient(135deg,#0f172a,#1e293b);
  }
  .summary-card.green{
    background:linear-gradient(135deg,#16a34a,#15803d);
  }
  .search-box{
    margin-bottom:14px;
  }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }
  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }
  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{
    display:none;
  }
  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }
  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }
  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }
  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }
  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }
  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }
  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }
  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }
  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }
  .wallet-card small,.mini-card small,.manager-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong{
    font-size:17px;
    font-weight:800;
  }
  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    grid-template-columns:repeat(4,1fr);
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:86px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  @media (max-width:360px){
    .factory-phone{ padding:16px 12px 95px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .manager-grid{ grid-template-columns:1fr 1fr; }
  }
</style>

<script>
(function(){
  const services = [
    { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 },
    { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 },
    { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 },
    { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 },
    { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 },
    { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 }
  ];

  let selectedService = null;
  let currentQty = 1;

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان" }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const chipsBox = document.getElementById("serviceChips");
  const selectedServiceBox = document.getElementById("selectedServiceBox");
  const todayItems = document.getElementById("todayItems");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const serviceSearch = document.getElementById("serviceSearch");
  const toast = document.getElementById("toast");

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getFilteredServices(){
    const q = normalizeText(serviceSearch.value.trim());
    if(!q) return services;
    return services.filter(s => normalizeText(s.name).includes(q));
  }

  function renderChips(list = services){
    chipsBox.innerHTML = "";
    list.forEach(service => {
      const btn = document.createElement("button");
      btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : "");
      btn.textContent = service.name;
      btn.onclick = function(){
        selectedService = service;
        currentQty = 1;
        renderChips(getFilteredServices());
        renderSelectedService();
      };
      chipsBox.appendChild(btn);
    });
  }

  function renderSelectedService(){
    if(!selectedService){
      selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`;
      return;
    }

    selectedServiceBox.innerHTML = `
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>${selectedService.name}</h3>
            <p>قیمت واحد: ${money(selectedService.price)}</p>
          </div>
          <div class="price-badge">${money(selectedService.price * currentQty)}</div>
        </div>

        <div class="counter">
          <button type="button" id="minusQty">−</button>
          <input type="number" id="qtyInput" min="1" value="${currentQty}">
          <button type="button" id="plusQty">+</button>
        </div>

        <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button>
      </div>
    `;

    document.getElementById("minusQty").onclick = function(){
      currentQty = Math.max(1, currentQty - 1);
      renderSelectedService();
    };

    document.getElementById("plusQty").onclick = function(){
      currentQty++;
      renderSelectedService();
    };

    document.getElementById("qtyInput").oninput = function(e){
      currentQty = Math.max(1, parseInt(e.target.value || "1"));
      renderSelectedService();
    };

    document.getElementById("addTodayBtn").onclick = function(){
      entries.unshift({
        id: Date.now(),
        serviceId: selectedService.id,
        name: selectedService.name,
        price: selectedService.price,
        qty: currentQty,
        status: "pending",
        worker: "عرفان"
      });
      currentQty = 1;
      renderAll();
      showToast("ثبت جدید اضافه شد");
    };
  }

  function renderTodayItems(){
    if(entries.length === 0){
      todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`;
      return;
    }

    todayItems.innerHTML = "";
    entries.forEach((item, index) => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(item.qty * item.price)}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      `;
      row.querySelector(".btn-remove").onclick = function(){
        entries.splice(index, 1);
        renderAll();
        showToast("آیتم حذف شد");
      };
      todayItems.appendChild(row);
    });
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(item.qty * item.price)}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + (item.qty * item.price), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + (item.qty * item.price), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + (item.qty * item.price), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد | ${money(item.qty * item.price)}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + item.qty * item.price, 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += item.qty * item.price;
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderRegisterSummary(){
    const totalQty = entries.reduce((sum, item) => sum + item.qty, 0);
    const totalPrice = entries.reduce((sum, item) => sum + item.qty * item.price, 0);

    document.getElementById("regTotalQty").textContent = toFa(totalQty);
    document.getElementById("regTotalPrice").textContent = money(totalPrice);
  }

  function renderAll(){
    renderChips(getFilteredServices());
    renderSelectedService();
    renderTodayItems();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderRegisterSummary();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function(){
    renderChips(getFilteredServices());
  });

  renderAll();
})();
</script>
برنامه مارگر ۲۴
TEXT - 2026-05-05 21:55:15
<div class="factory-app" dir="rtl"> <div class="factory-phone"> <!-- Header --> <div class="app-header"> <div> <h1>سامانه ثبت کارکرد</h1> <p>نسخه نمایشی موبایلی</p> </div> <div class="demo-badge">DEMO</div> </div> <!-- Pages --> <div class="pages-wrap"> <!-- ثبت کارکرد --> <section class="page active" id="page-register"> <div class="page-title"> <div> <h2>ثبت کارکرد</h2> <span>کارگر: عرفان</span> </div> </div> <div class="summary-grid"> <div class="summary-card dark"> <small>تعداد کل امروز</small> <strong id="regTotalQty">۰</strong> </div> <div class="summary-card green"> <small>جمع مبلغ امروز</small> <strong id="regTotalPrice">۰ تومان</strong> </div> </div> <div class="search-box"> <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..."> </div> <div class="section-title">خدمات پرکاربرد</div> <div class="chips" id="serviceChips"></div> <div id="selectedServiceBox" class="selected-box"> <div class="empty-box">یک خدمت را انتخاب کن</div> </div> <div class="section-title">ثبت‌های امروز</div> <div class="list-box" id="todayItems"> <div class="empty-list">هنوز چیزی ثبت نشده</div> </div> </section> <!-- حساب کارگر --> <section class="page" id="page-worker"> <div class="page-title"> <div> <h2>حساب کارگر</h2> <span>خلاصه مالی و ثبت‌ها</span> </div> </div> <div class="wallet-card"> <div> <small>جمع کل کارکرد</small> <strong id="workerTotalAmount">۰ تومان</strong> </div> <div> <small>تعداد آیتم‌ها</small> <strong id="workerTotalCount">۰</strong> </div> </div> <div class="mini-grid"> <div class="mini-card"> <small>تایید شده</small> <strong id="approvedAmount">۰ تومان</strong> </div> <div class="mini-card warning"> <small>در انتظار تایید</small> <strong id="pendingAmount">۰ تومان</strong> </div> </div> <div class="section-title">ریز حساب کارگر</div> <div class="list-box" id="workerAccountList"> <div class="empty-list">موردی وجود ندارد</div> </div> </section> <!-- تایید مدیر --> <section class="page" id="page-approve"> <div class="page-title"> <div> <h2>تایید مدیر</h2> <span>بررسی ثبت‌ها</span> </div> </div> <div class="mini-grid three"> <div class="mini-card"> <small>در انتظار</small> <strong id="pendingCount">۰</strong> </div> <div class="mini-card success"> <small>تایید شده</small> <strong id="approvedCount">۰</strong> </div> <div class="mini-card danger"> <small>رد شده</small> <strong id="rejectedCount">۰</strong> </div> </div> <div class="section-title">لیست بررسی مدیر</div> <div class="list-box" id="approvalList"> <div class="empty-list">چیزی برای بررسی نیست</div> </div> </section> <!-- مدیر تولیدی --> <section class="page" id="page-manager"> <div class="page-title"> <div> <h2>مدیر تولیدی</h2> <span>خلاصه عملیات روز</span> </div> </div> <div class="manager-grid"> <div class="manager-card blue"> <small>کل ثبت‌ها</small> <strong id="managerRecords">۰</strong> </div> <div class="manager-card violet"> <small>کل مبلغ</small> <strong id="managerAmount">۰ تومان</strong> </div> <div class="manager-card orange"> <small>تعداد خدمات</small> <strong id="managerServices">۰</strong> </div> <div class="manager-card green"> <small>کارگران فعال</small> <strong>۱</strong> </div> </div> <div class="section-title">خلاصه خدمات امروز</div> <div class="list-box" id="managerServiceSummary"> <div class="empty-list">هنوز آماری ثبت نشده</div> </div> </section> </div> <!-- Bottom Navigation --> <div class="bottom-nav"> <button class="tab-btn active" data-page="register">ثبت کارکرد</button> <button class="tab-btn" data-page="worker">حساب کارگر</button> <button class="tab-btn" data-page="approve">تایید مدیر</button> <button class="tab-btn" data-page="manager">مدیر تولیدی</button> </div> <div class="toast" id="toast">انجام شد</div> </div> </div> <style> .factory-app{ background:#eef2f7; min-height:100vh; display:flex; justify-content:center; font-family:Tahoma, Arial, sans-serif; } .factory-phone{ width:100%; max-width:430px; min-height:100vh; background:#f8fafc; position:relative; padding:18px 14px 95px; box-sizing:border-box; } .app-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } .app-header h1{ margin:0; font-size:22px; color:#0f172a; font-weight:800; } .app-header p{ margin:6px 0 0; color:#64748b; font-size:13px; } .demo-badge{ background:#111827; color:#fff; padding:8px 12px; border-radius:999px; font-size:11px; font-weight:800; } .page{ display:none; } .page.active{ display:block; } .page-title{ margin-bottom:16px; } .page-title h2{ margin:0 0 6px; font-size:20px; color:#111827; font-weight:800; } .page-title span{ color:#6b7280; font-size:13px; } .summary-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } .summary-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .summary-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .summary-card strong{ font-size:18px; font-weight:800; } .summary-card.dark{ background:linear-gradient(135deg,#0f172a,#1e293b); } .summary-card.green{ background:linear-gradient(135deg,#16a34a,#15803d); } .search-box{ margin-bottom:14px; } .search-box input{ width:100%; height:54px; border:none; outline:none; border-radius:18px; padding:0 16px; box-sizing:border-box; background:#fff; box-shadow:0 8px 24px rgba(15,23,42,.06); font-size:15px; } .section-title{ font-size:13px; color:#6b7280; font-weight:700; margin:12px 2px 10px; } .chips{ display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:none; } .chips::-webkit-scrollbar{ display:none; } .chip{ border:none; background:#fff; color:#111827; border-radius:999px; padding:12px 15px; font-size:13px; font-weight:700; box-shadow:0 8px 20px rgba(15,23,42,.06); white-space:nowrap; cursor:pointer; } .chip.active{ background:#2563eb; color:#fff; } .selected-box{ margin-top:14px; margin-bottom:10px; min-height:110px; } .empty-box,.empty-list{ background:#fff; border-radius:20px; min-height:90px; display:flex; align-items:center; justify-content:center; color:#94a3b8; font-size:13px; box-shadow:0 8px 24px rgba(15,23,42,.05); text-align:center; padding:12px; box-sizing:border-box; } .service-card{ background:#fff; border-radius:22px; padding:16px; box-shadow:0 10px 28px rgba(15,23,42,.08); } .service-card-top{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:14px; } .service-card h3{ margin:0 0 6px; font-size:17px; color:#111827; font-weight:800; } .service-card p{ margin:0; color:#6b7280; font-size:13px; } .price-badge{ background:#eff6ff; color:#2563eb; padding:8px 10px; border-radius:999px; font-size:12px; font-weight:800; white-space:nowrap; } .counter{ display:grid; grid-template-columns:54px 1fr 54px; gap:10px; margin-bottom:12px; } .counter button{ height:52px; border:none; background:#f1f5f9; border-radius:16px; font-size:24px; font-weight:800; cursor:pointer; } .counter input{ height:52px; border:none; background:#f8fafc; border-radius:16px; text-align:center; font-size:21px; font-weight:800; outline:none; } .add-btn{ width:100%; height:54px; border:none; background:#16a34a; color:#fff; border-radius:18px; font-size:15px; font-weight:800; cursor:pointer; } .list-box{ display:flex; flex-direction:column; gap:10px; } .list-row{ background:#fff; border-radius:18px; padding:13px 14px; display:grid; grid-template-columns:1fr auto; gap:10px; align-items:center; box-shadow:0 8px 20px rgba(15,23,42,.05); } .list-row h4{ margin:0 0 6px; color:#111827; font-size:14px; font-weight:800; } .list-row p{ margin:0; color:#6b7280; font-size:12px; line-height:1.8; } .row-actions{ display:flex; gap:8px; flex-direction:column; } .small-btn{ border:none; border-radius:12px; padding:9px 10px; font-size:12px; font-weight:800; cursor:pointer; min-width:72px; } .btn-remove{ background:#fee2e2; color:#dc2626; } .btn-approve{ background:#dcfce7; color:#15803d; } .btn-reject{ background:#fef3c7; color:#b45309; } .wallet-card{ background:linear-gradient(135deg,#1d4ed8,#2563eb); color:#fff; border-radius:24px; padding:18px; display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; box-shadow:0 12px 30px rgba(37,99,235,.22); } .wallet-card small,.mini-card small,.manager-card small{ display:block; font-size:12px; margin-bottom:8px; opacity:.9; } .wallet-card strong,.mini-card strong,.manager-card strong{ font-size:17px; font-weight:800; } .mini-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .mini-grid.three{ grid-template-columns:1fr 1fr 1fr; } .mini-card{ background:#fff; border-radius:20px; padding:15px; box-shadow:0 8px 24px rgba(15,23,42,.05); } .mini-card.warning{ background:#fff7ed; color:#9a3412; } .mini-card.success{ background:#f0fdf4; color:#166534; } .mini-card.danger{ background:#fef2f2; color:#b91c1c; } .manager-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:14px; } .manager-card{ border-radius:22px; padding:16px; color:#fff; box-shadow:0 10px 28px rgba(15,23,42,.08); } .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); } .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); } .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); } .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); } .status{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:800; margin-top:6px; } .status.pending{ background:#fef3c7; color:#92400e; } .status.approved{ background:#dcfce7; color:#166534; } .status.rejected{ background:#fee2e2; color:#991b1b; } .bottom-nav{ position:fixed; bottom:0; right:50%; transform:translateX(50%); width:100%; max-width:430px; display:grid; grid-template-columns:repeat(4,1fr); gap:8px; padding:12px 10px 16px; box-sizing:border-box; background:rgba(248,250,252,.95); backdrop-filter:blur(12px); border-top:1px solid #e5e7eb; } .tab-btn{ border:none; background:#e2e8f0; color:#334155; min-height:52px; border-radius:16px; font-size:12px; font-weight:800; cursor:pointer; padding:6px; } .tab-btn.active{ background:#2563eb; color:#fff; box-shadow:0 8px 20px rgba(37,99,235,.25); } .toast{ position:fixed; bottom:86px; right:50%; transform:translateX(50%) translateY(20px); background:#111827; color:#fff; padding:12px 18px; border-radius:999px; font-size:13px; opacity:0; pointer-events:none; transition:.25s ease; z-index:999; } .toast.show{ opacity:1; transform:translateX(50%) translateY(0); } @media (max-width:360px){ .factory-phone{ padding:16px 12px 95px; } .mini-grid.three{ grid-template-columns:1fr; } .manager-grid{ grid-template-columns:1fr 1fr; } } </style> <script> (function(){ const services = [ { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 }, { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 }, { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 }, { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 }, { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 }, { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 } ]; let selectedService = null; let currentQty = 1; let entries = [ { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان" }, { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان" } ]; const tabs = document.querySelectorAll(".tab-btn"); const pages = document.querySelectorAll(".page"); const chipsBox = document.getElementById("serviceChips"); const selectedServiceBox = document.getElementById("selectedServiceBox"); const todayItems = document.getElementById("todayItems"); const workerAccountList = document.getElementById("workerAccountList"); const approvalList = document.getElementById("approvalList"); const managerServiceSummary = document.getElementById("managerServiceSummary"); const serviceSearch = document.getElementById("serviceSearch"); const toast = document.getElementById("toast"); function toFa(num){ return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num){ return toFa(Number(num).toLocaleString("en-US")) + " تومان"; } function normalizeText(text){ return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function showToast(text){ toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function getFilteredServices(){ const q = normalizeText(serviceSearch.value.trim()); if(!q) return services; return services.filter(s => normalizeText(s.name).includes(q)); } function renderChips(list = services){ chipsBox.innerHTML = ""; list.forEach(service => { const btn = document.createElement("button"); btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : ""); btn.textContent = service.name; btn.onclick = function(){ selectedService = service; currentQty = 1; renderChips(getFilteredServices()); renderSelectedService(); }; chipsBox.appendChild(btn); }); } function renderSelectedService(){ if(!selectedService){ selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`; return; } selectedServiceBox.innerHTML = ` <div class="service-card"> <div class="service-card-top"> <div> <h3>${selectedService.name}</h3> <p>قیمت واحد: ${money(selectedService.price)}</p> </div> <div class="price-badge">${money(selectedService.price * currentQty)}</div> </div> <div class="counter"> <button type="button" id="minusQty">−</button> <input type="number" id="qtyInput" min="1" value="${currentQty}"> <button type="button" id="plusQty">+</button> </div> <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button> </div> `; document.getElementById("minusQty").onclick = function(){ currentQty = Math.max(1, currentQty - 1); renderSelectedService(); }; document.getElementById("plusQty").onclick = function(){ currentQty++; renderSelectedService(); }; document.getElementById("qtyInput").oninput = function(e){ currentQty = Math.max(1, parseInt(e.target.value || "1")); renderSelectedService(); }; document.getElementById("addTodayBtn").onclick = function(){ entries.unshift({ id: Date.now(), serviceId: selectedService.id, name: selectedService.name, price: selectedService.price, qty: currentQty, status: "pending", worker: "عرفان" }); currentQty = 1; renderAll(); showToast("ثبت جدید اضافه شد"); }; } function renderTodayItems(){ if(entries.length === 0){ todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`; return; } todayItems.innerHTML = ""; entries.forEach((item, index) => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(item.qty * item.price)} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-remove" type="button">حذف</button> </div> `; row.querySelector(".btn-remove").onclick = function(){ entries.splice(index, 1); renderAll(); showToast("آیتم حذف شد"); }; todayItems.appendChild(row); }); } function renderWorkerPage(){ if(entries.length === 0){ workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`; } else { workerAccountList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p> تعداد: ${toFa(item.qty)} عدد <br> مبلغ: ${money(item.qty * item.price)} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div></div> `; workerAccountList.appendChild(row); }); } const totalAmount = entries.reduce((sum, item) => sum + (item.qty * item.price), 0); const totalCount = entries.reduce((sum, item) => sum + item.qty, 0); const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + (item.qty * item.price), 0); const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + (item.qty * item.price), 0); document.getElementById("workerTotalAmount").textContent = money(totalAmount); document.getElementById("workerTotalCount").textContent = toFa(totalCount); document.getElementById("approvedAmount").textContent = money(approvedAmount); document.getElementById("pendingAmount").textContent = money(pendingAmount); } function renderApprovalPage(){ const pending = entries.filter(i => i.status === "pending"); const approved = entries.filter(i => i.status === "approved"); const rejected = entries.filter(i => i.status === "rejected"); document.getElementById("pendingCount").textContent = toFa(pending.length); document.getElementById("approvedCount").textContent = toFa(approved.length); document.getElementById("rejectedCount").textContent = toFa(rejected.length); if(entries.length === 0){ approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`; return; } approvalList.innerHTML = ""; entries.forEach(item => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${item.worker} - ${item.name}</h4> <p> ${toFa(item.qty)} عدد | ${money(item.qty * item.price)} <br> <span class="status ${item.status}"> ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"} </span> </p> </div> <div class="row-actions"> <button class="small-btn btn-approve" type="button">تایید</button> <button class="small-btn btn-reject" type="button">رد</button> </div> `; row.querySelector(".btn-approve").onclick = function(){ item.status = "approved"; renderAll(); showToast("آیتم تایید شد"); }; row.querySelector(".btn-reject").onclick = function(){ item.status = "rejected"; renderAll(); showToast("آیتم رد شد"); }; approvalList.appendChild(row); }); } function renderManagerPage(){ const totalRecords = entries.length; const totalAmount = entries.reduce((sum, item) => sum + item.qty * item.price, 0); const totalServices = [...new Set(entries.map(i => i.serviceId))].length; document.getElementById("managerRecords").textContent = toFa(totalRecords); document.getElementById("managerAmount").textContent = money(totalAmount); document.getElementById("managerServices").textContent = toFa(totalServices); if(entries.length === 0){ managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`; return; } const grouped = {}; entries.forEach(item => { if(!grouped[item.name]){ grouped[item.name] = { qty: 0, amount: 0 }; } grouped[item.name].qty += item.qty; grouped[item.name].amount += item.qty * item.price; }); managerServiceSummary.innerHTML = ""; Object.keys(grouped).forEach(name => { const row = document.createElement("div"); row.className = "list-row"; row.innerHTML = ` <div> <h4>${name}</h4> <p> تعداد کل: ${toFa(grouped[name].qty)} عدد <br> جمع مبلغ: ${money(grouped[name].amount)} </p> </div> <div></div> `; managerServiceSummary.appendChild(row); }); } function renderRegisterSummary(){ const totalQty = entries.reduce((sum, item) => sum + item.qty, 0); const totalPrice = entries.reduce((sum, item) => sum + item.qty * item.price, 0); document.getElementById("regTotalQty").textContent = toFa(totalQty); document.getElementById("regTotalPrice").textContent = money(totalPrice); } function renderAll(){ renderChips(getFilteredServices()); renderSelectedService(); renderTodayItems(); renderWorkerPage(); renderApprovalPage(); renderManagerPage(); renderRegisterSummary(); } tabs.forEach(btn => { btn.addEventListener("click", function(){ const pageName = this.getAttribute("data-page"); tabs.forEach(b => b.classList.remove("active")); this.classList.add("active"); pages.forEach(page => page.classList.remove("active")); document.getElementById("page-" + pageName).classList.add("active"); }); }); serviceSearch.addEventListener("input", function(){ renderChips(getFilteredServices()); }); renderAll(); })(); </script>
<div class="factory-app" dir="rtl">
  <div class="factory-phone">

    <!-- Header -->
    <div class="app-header">
      <div>
        <h1>سامانه ثبت کارکرد</h1>
        <p>نسخه نمایشی موبایلی</p>
      </div>
      <div class="demo-badge">DEMO</div>
    </div>

    <!-- Pages -->
    <div class="pages-wrap">

      <!-- ثبت کارکرد -->
      <section class="page active" id="page-register">
        <div class="page-title">
          <div>
            <h2>ثبت کارکرد</h2>
            <span>کارگر: عرفان</span>
          </div>
        </div>

        <div class="summary-grid">
          <div class="summary-card dark">
            <small>تعداد کل امروز</small>
            <strong id="regTotalQty">۰</strong>
          </div>
          <div class="summary-card green">
            <small>جمع مبلغ امروز</small>
            <strong id="regTotalPrice">۰ تومان</strong>
          </div>
        </div>

        <div class="search-box">
          <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت...">
        </div>

        <div class="section-title">خدمات پرکاربرد</div>
        <div class="chips" id="serviceChips"></div>

        <div id="selectedServiceBox" class="selected-box">
          <div class="empty-box">یک خدمت را انتخاب کن</div>
        </div>

        <div class="section-title">ثبت‌های امروز</div>
        <div class="list-box" id="todayItems">
          <div class="empty-list">هنوز چیزی ثبت نشده</div>
        </div>
      </section>

      <!-- حساب کارگر -->
      <section class="page" id="page-worker">
        <div class="page-title">
          <div>
            <h2>حساب کارگر</h2>
            <span>خلاصه مالی و ثبت‌ها</span>
          </div>
        </div>

        <div class="wallet-card">
          <div>
            <small>جمع کل کارکرد</small>
            <strong id="workerTotalAmount">۰ تومان</strong>
          </div>
          <div>
            <small>تعداد آیتم‌ها</small>
            <strong id="workerTotalCount">۰</strong>
          </div>
        </div>

        <div class="mini-grid">
          <div class="mini-card">
            <small>تایید شده</small>
            <strong id="approvedAmount">۰ تومان</strong>
          </div>
          <div class="mini-card warning">
            <small>در انتظار تایید</small>
            <strong id="pendingAmount">۰ تومان</strong>
          </div>
        </div>

        <div class="section-title">ریز حساب کارگر</div>
        <div class="list-box" id="workerAccountList">
          <div class="empty-list">موردی وجود ندارد</div>
        </div>
      </section>

      <!-- تایید مدیر -->
      <section class="page" id="page-approve">
        <div class="page-title">
          <div>
            <h2>تایید مدیر</h2>
            <span>بررسی ثبت‌ها</span>
          </div>
        </div>

        <div class="mini-grid three">
          <div class="mini-card">
            <small>در انتظار</small>
            <strong id="pendingCount">۰</strong>
          </div>
          <div class="mini-card success">
            <small>تایید شده</small>
            <strong id="approvedCount">۰</strong>
          </div>
          <div class="mini-card danger">
            <small>رد شده</small>
            <strong id="rejectedCount">۰</strong>
          </div>
        </div>

        <div class="section-title">لیست بررسی مدیر</div>
        <div class="list-box" id="approvalList">
          <div class="empty-list">چیزی برای بررسی نیست</div>
        </div>
      </section>

      <!-- مدیر تولیدی -->
      <section class="page" id="page-manager">
        <div class="page-title">
          <div>
            <h2>مدیر تولیدی</h2>
            <span>خلاصه عملیات روز</span>
          </div>
        </div>

        <div class="manager-grid">
          <div class="manager-card blue">
            <small>کل ثبت‌ها</small>
            <strong id="managerRecords">۰</strong>
          </div>
          <div class="manager-card violet">
            <small>کل مبلغ</small>
            <strong id="managerAmount">۰ تومان</strong>
          </div>
          <div class="manager-card orange">
            <small>تعداد خدمات</small>
            <strong id="managerServices">۰</strong>
          </div>
          <div class="manager-card green">
            <small>کارگران فعال</small>
            <strong>۱</strong>
          </div>
        </div>

        <div class="section-title">خلاصه خدمات امروز</div>
        <div class="list-box" id="managerServiceSummary">
          <div class="empty-list">هنوز آماری ثبت نشده</div>
        </div>
      </section>
    </div>

    <!-- Bottom Navigation -->
    <div class="bottom-nav">
      <button class="tab-btn active" data-page="register">ثبت کارکرد</button>
      <button class="tab-btn" data-page="worker">حساب کارگر</button>
      <button class="tab-btn" data-page="approve">تایید مدیر</button>
      <button class="tab-btn" data-page="manager">مدیر تولیدی</button>
    </div>

    <div class="toast" id="toast">انجام شد</div>
  </div>
</div>

<style>
  .factory-app{
    background:#eef2f7;
    min-height:100vh;
    display:flex;
    justify-content:center;
    font-family:Tahoma, Arial, sans-serif;
  }
  .factory-phone{
    width:100%;
    max-width:430px;
    min-height:100vh;
    background:#f8fafc;
    position:relative;
    padding:18px 14px 95px;
    box-sizing:border-box;
  }
  .app-header{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:18px;
  }
  .app-header h1{
    margin:0;
    font-size:22px;
    color:#0f172a;
    font-weight:800;
  }
  .app-header p{
    margin:6px 0 0;
    color:#64748b;
    font-size:13px;
  }
  .demo-badge{
    background:#111827;
    color:#fff;
    padding:8px 12px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
  }
  .page{
    display:none;
  }
  .page.active{
    display:block;
  }
  .page-title{
    margin-bottom:16px;
  }
  .page-title h2{
    margin:0 0 6px;
    font-size:20px;
    color:#111827;
    font-weight:800;
  }
  .page-title span{
    color:#6b7280;
    font-size:13px;
  }
  .summary-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:16px;
  }
  .summary-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .summary-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .summary-card strong{
    font-size:18px;
    font-weight:800;
  }
  .summary-card.dark{
    background:linear-gradient(135deg,#0f172a,#1e293b);
  }
  .summary-card.green{
    background:linear-gradient(135deg,#16a34a,#15803d);
  }
  .search-box{
    margin-bottom:14px;
  }
  .search-box input{
    width:100%;
    height:54px;
    border:none;
    outline:none;
    border-radius:18px;
    padding:0 16px;
    box-sizing:border-box;
    background:#fff;
    box-shadow:0 8px 24px rgba(15,23,42,.06);
    font-size:15px;
  }
  .section-title{
    font-size:13px;
    color:#6b7280;
    font-weight:700;
    margin:12px 2px 10px;
  }
  .chips{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:6px;
    scrollbar-width:none;
  }
  .chips::-webkit-scrollbar{
    display:none;
  }
  .chip{
    border:none;
    background:#fff;
    color:#111827;
    border-radius:999px;
    padding:12px 15px;
    font-size:13px;
    font-weight:700;
    box-shadow:0 8px 20px rgba(15,23,42,.06);
    white-space:nowrap;
    cursor:pointer;
  }
  .chip.active{
    background:#2563eb;
    color:#fff;
  }
  .selected-box{
    margin-top:14px;
    margin-bottom:10px;
    min-height:110px;
  }
  .empty-box,.empty-list{
    background:#fff;
    border-radius:20px;
    min-height:90px;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#94a3b8;
    font-size:13px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
    text-align:center;
    padding:12px;
    box-sizing:border-box;
  }
  .service-card{
    background:#fff;
    border-radius:22px;
    padding:16px;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .service-card-top{
    display:flex;
    justify-content:space-between;
    gap:10px;
    align-items:flex-start;
    margin-bottom:14px;
  }
  .service-card h3{
    margin:0 0 6px;
    font-size:17px;
    color:#111827;
    font-weight:800;
  }
  .service-card p{
    margin:0;
    color:#6b7280;
    font-size:13px;
  }
  .price-badge{
    background:#eff6ff;
    color:#2563eb;
    padding:8px 10px;
    border-radius:999px;
    font-size:12px;
    font-weight:800;
    white-space:nowrap;
  }
  .counter{
    display:grid;
    grid-template-columns:54px 1fr 54px;
    gap:10px;
    margin-bottom:12px;
  }
  .counter button{
    height:52px;
    border:none;
    background:#f1f5f9;
    border-radius:16px;
    font-size:24px;
    font-weight:800;
    cursor:pointer;
  }
  .counter input{
    height:52px;
    border:none;
    background:#f8fafc;
    border-radius:16px;
    text-align:center;
    font-size:21px;
    font-weight:800;
    outline:none;
  }
  .add-btn{
    width:100%;
    height:54px;
    border:none;
    background:#16a34a;
    color:#fff;
    border-radius:18px;
    font-size:15px;
    font-weight:800;
    cursor:pointer;
  }
  .list-box{
    display:flex;
    flex-direction:column;
    gap:10px;
  }
  .list-row{
    background:#fff;
    border-radius:18px;
    padding:13px 14px;
    display:grid;
    grid-template-columns:1fr auto;
    gap:10px;
    align-items:center;
    box-shadow:0 8px 20px rgba(15,23,42,.05);
  }
  .list-row h4{
    margin:0 0 6px;
    color:#111827;
    font-size:14px;
    font-weight:800;
  }
  .list-row p{
    margin:0;
    color:#6b7280;
    font-size:12px;
    line-height:1.8;
  }
  .row-actions{
    display:flex;
    gap:8px;
    flex-direction:column;
  }
  .small-btn{
    border:none;
    border-radius:12px;
    padding:9px 10px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    min-width:72px;
  }
  .btn-remove{ background:#fee2e2; color:#dc2626; }
  .btn-approve{ background:#dcfce7; color:#15803d; }
  .btn-reject{ background:#fef3c7; color:#b45309; }

  .wallet-card{
    background:linear-gradient(135deg,#1d4ed8,#2563eb);
    color:#fff;
    border-radius:24px;
    padding:18px;
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
    box-shadow:0 12px 30px rgba(37,99,235,.22);
  }
  .wallet-card small,.mini-card small,.manager-card small{
    display:block;
    font-size:12px;
    margin-bottom:8px;
    opacity:.9;
  }
  .wallet-card strong,.mini-card strong,.manager-card strong{
    font-size:17px;
    font-weight:800;
  }
  .mini-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .mini-grid.three{
    grid-template-columns:1fr 1fr 1fr;
  }
  .mini-card{
    background:#fff;
    border-radius:20px;
    padding:15px;
    box-shadow:0 8px 24px rgba(15,23,42,.05);
  }
  .mini-card.warning{ background:#fff7ed; color:#9a3412; }
  .mini-card.success{ background:#f0fdf4; color:#166534; }
  .mini-card.danger{ background:#fef2f2; color:#b91c1c; }

  .manager-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
    margin-bottom:14px;
  }
  .manager-card{
    border-radius:22px;
    padding:16px;
    color:#fff;
    box-shadow:0 10px 28px rgba(15,23,42,.08);
  }
  .manager-card.blue{ background:linear-gradient(135deg,#0284c7,#0369a1); }
  .manager-card.violet{ background:linear-gradient(135deg,#7c3aed,#6d28d9); }
  .manager-card.orange{ background:linear-gradient(135deg,#f59e0b,#d97706); }
  .manager-card.green{ background:linear-gradient(135deg,#22c55e,#16a34a); }

  .status{
    display:inline-block;
    padding:4px 10px;
    border-radius:999px;
    font-size:11px;
    font-weight:800;
    margin-top:6px;
  }
  .status.pending{ background:#fef3c7; color:#92400e; }
  .status.approved{ background:#dcfce7; color:#166534; }
  .status.rejected{ background:#fee2e2; color:#991b1b; }

  .bottom-nav{
    position:fixed;
    bottom:0;
    right:50%;
    transform:translateX(50%);
    width:100%;
    max-width:430px;
    display:grid;
    grid-template-columns:repeat(4,1fr);
    gap:8px;
    padding:12px 10px 16px;
    box-sizing:border-box;
    background:rgba(248,250,252,.95);
    backdrop-filter:blur(12px);
    border-top:1px solid #e5e7eb;
  }
  .tab-btn{
    border:none;
    background:#e2e8f0;
    color:#334155;
    min-height:52px;
    border-radius:16px;
    font-size:12px;
    font-weight:800;
    cursor:pointer;
    padding:6px;
  }
  .tab-btn.active{
    background:#2563eb;
    color:#fff;
    box-shadow:0 8px 20px rgba(37,99,235,.25);
  }

  .toast{
    position:fixed;
    bottom:86px;
    right:50%;
    transform:translateX(50%) translateY(20px);
    background:#111827;
    color:#fff;
    padding:12px 18px;
    border-radius:999px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:.25s ease;
    z-index:999;
  }
  .toast.show{
    opacity:1;
    transform:translateX(50%) translateY(0);
  }

  @media (max-width:360px){
    .factory-phone{ padding:16px 12px 95px; }
    .mini-grid.three{ grid-template-columns:1fr; }
    .manager-grid{ grid-template-columns:1fr 1fr; }
  }
</style>

<script>
(function(){
  const services = [
    { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 },
    { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 },
    { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 },
    { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 },
    { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 },
    { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 }
  ];

  let selectedService = null;
  let currentQty = 1;

  let entries = [
    { id: 1001, serviceId: 1, name: "میز لبه‌دار ۳۵", price: 95000, qty: 2, status: "pending", worker: "عرفان" },
    { id: 1002, serviceId: 3, name: "میز لبه‌دار ۵۰", price: 110000, qty: 1, status: "approved", worker: "عرفان" }
  ];

  const tabs = document.querySelectorAll(".tab-btn");
  const pages = document.querySelectorAll(".page");
  const chipsBox = document.getElementById("serviceChips");
  const selectedServiceBox = document.getElementById("selectedServiceBox");
  const todayItems = document.getElementById("todayItems");
  const workerAccountList = document.getElementById("workerAccountList");
  const approvalList = document.getElementById("approvalList");
  const managerServiceSummary = document.getElementById("managerServiceSummary");
  const serviceSearch = document.getElementById("serviceSearch");
  const toast = document.getElementById("toast");

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

  function money(num){
    return toFa(Number(num).toLocaleString("en-US")) + " تومان";
  }

  function normalizeText(text){
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function showToast(text){
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function getFilteredServices(){
    const q = normalizeText(serviceSearch.value.trim());
    if(!q) return services;
    return services.filter(s => normalizeText(s.name).includes(q));
  }

  function renderChips(list = services){
    chipsBox.innerHTML = "";
    list.forEach(service => {
      const btn = document.createElement("button");
      btn.className = "chip" + (selectedService && selectedService.id === service.id ? " active" : "");
      btn.textContent = service.name;
      btn.onclick = function(){
        selectedService = service;
        currentQty = 1;
        renderChips(getFilteredServices());
        renderSelectedService();
      };
      chipsBox.appendChild(btn);
    });
  }

  function renderSelectedService(){
    if(!selectedService){
      selectedServiceBox.innerHTML = `<div class="empty-box">یک خدمت را انتخاب کن</div>`;
      return;
    }

    selectedServiceBox.innerHTML = `
      <div class="service-card">
        <div class="service-card-top">
          <div>
            <h3>${selectedService.name}</h3>
            <p>قیمت واحد: ${money(selectedService.price)}</p>
          </div>
          <div class="price-badge">${money(selectedService.price * currentQty)}</div>
        </div>

        <div class="counter">
          <button type="button" id="minusQty">−</button>
          <input type="number" id="qtyInput" min="1" value="${currentQty}">
          <button type="button" id="plusQty">+</button>
        </div>

        <button type="button" class="add-btn" id="addTodayBtn">افزودن به لیست امروز</button>
      </div>
    `;

    document.getElementById("minusQty").onclick = function(){
      currentQty = Math.max(1, currentQty - 1);
      renderSelectedService();
    };

    document.getElementById("plusQty").onclick = function(){
      currentQty++;
      renderSelectedService();
    };

    document.getElementById("qtyInput").oninput = function(e){
      currentQty = Math.max(1, parseInt(e.target.value || "1"));
      renderSelectedService();
    };

    document.getElementById("addTodayBtn").onclick = function(){
      entries.unshift({
        id: Date.now(),
        serviceId: selectedService.id,
        name: selectedService.name,
        price: selectedService.price,
        qty: currentQty,
        status: "pending",
        worker: "عرفان"
      });
      currentQty = 1;
      renderAll();
      showToast("ثبت جدید اضافه شد");
    };
  }

  function renderTodayItems(){
    if(entries.length === 0){
      todayItems.innerHTML = `<div class="empty-list">هنوز چیزی ثبت نشده</div>`;
      return;
    }

    todayItems.innerHTML = "";
    entries.forEach((item, index) => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد × ${money(item.price)} = ${money(item.qty * item.price)}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-remove" type="button">حذف</button>
        </div>
      `;
      row.querySelector(".btn-remove").onclick = function(){
        entries.splice(index, 1);
        renderAll();
        showToast("آیتم حذف شد");
      };
      todayItems.appendChild(row);
    });
  }

  function renderWorkerPage(){
    if(entries.length === 0){
      workerAccountList.innerHTML = `<div class="empty-list">موردی وجود ندارد</div>`;
    } else {
      workerAccountList.innerHTML = "";
      entries.forEach(item => {
        const row = document.createElement("div");
        row.className = "list-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>
              تعداد: ${toFa(item.qty)} عدد
              <br>
              مبلغ: ${money(item.qty * item.price)}
              <br>
              <span class="status ${item.status}">
                ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
              </span>
            </p>
          </div>
          <div></div>
        `;
        workerAccountList.appendChild(row);
      });
    }

    const totalAmount = entries.reduce((sum, item) => sum + (item.qty * item.price), 0);
    const totalCount = entries.reduce((sum, item) => sum + item.qty, 0);
    const approvedAmount = entries.filter(i => i.status === "approved").reduce((sum, item) => sum + (item.qty * item.price), 0);
    const pendingAmount = entries.filter(i => i.status === "pending").reduce((sum, item) => sum + (item.qty * item.price), 0);

    document.getElementById("workerTotalAmount").textContent = money(totalAmount);
    document.getElementById("workerTotalCount").textContent = toFa(totalCount);
    document.getElementById("approvedAmount").textContent = money(approvedAmount);
    document.getElementById("pendingAmount").textContent = money(pendingAmount);
  }

  function renderApprovalPage(){
    const pending = entries.filter(i => i.status === "pending");
    const approved = entries.filter(i => i.status === "approved");
    const rejected = entries.filter(i => i.status === "rejected");

    document.getElementById("pendingCount").textContent = toFa(pending.length);
    document.getElementById("approvedCount").textContent = toFa(approved.length);
    document.getElementById("rejectedCount").textContent = toFa(rejected.length);

    if(entries.length === 0){
      approvalList.innerHTML = `<div class="empty-list">چیزی برای بررسی نیست</div>`;
      return;
    }

    approvalList.innerHTML = "";
    entries.forEach(item => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${item.worker} - ${item.name}</h4>
          <p>
            ${toFa(item.qty)} عدد | ${money(item.qty * item.price)}
            <br>
            <span class="status ${item.status}">
              ${item.status === "pending" ? "در انتظار تایید" : item.status === "approved" ? "تایید شده" : "رد شده"}
            </span>
          </p>
        </div>
        <div class="row-actions">
          <button class="small-btn btn-approve" type="button">تایید</button>
          <button class="small-btn btn-reject" type="button">رد</button>
        </div>
      `;

      row.querySelector(".btn-approve").onclick = function(){
        item.status = "approved";
        renderAll();
        showToast("آیتم تایید شد");
      };

      row.querySelector(".btn-reject").onclick = function(){
        item.status = "rejected";
        renderAll();
        showToast("آیتم رد شد");
      };

      approvalList.appendChild(row);
    });
  }

  function renderManagerPage(){
    const totalRecords = entries.length;
    const totalAmount = entries.reduce((sum, item) => sum + item.qty * item.price, 0);
    const totalServices = [...new Set(entries.map(i => i.serviceId))].length;

    document.getElementById("managerRecords").textContent = toFa(totalRecords);
    document.getElementById("managerAmount").textContent = money(totalAmount);
    document.getElementById("managerServices").textContent = toFa(totalServices);

    if(entries.length === 0){
      managerServiceSummary.innerHTML = `<div class="empty-list">هنوز آماری ثبت نشده</div>`;
      return;
    }

    const grouped = {};
    entries.forEach(item => {
      if(!grouped[item.name]){
        grouped[item.name] = { qty: 0, amount: 0 };
      }
      grouped[item.name].qty += item.qty;
      grouped[item.name].amount += item.qty * item.price;
    });

    managerServiceSummary.innerHTML = "";
    Object.keys(grouped).forEach(name => {
      const row = document.createElement("div");
      row.className = "list-row";
      row.innerHTML = `
        <div>
          <h4>${name}</h4>
          <p>
            تعداد کل: ${toFa(grouped[name].qty)} عدد
            <br>
            جمع مبلغ: ${money(grouped[name].amount)}
          </p>
        </div>
        <div></div>
      `;
      managerServiceSummary.appendChild(row);
    });
  }

  function renderRegisterSummary(){
    const totalQty = entries.reduce((sum, item) => sum + item.qty, 0);
    const totalPrice = entries.reduce((sum, item) => sum + item.qty * item.price, 0);

    document.getElementById("regTotalQty").textContent = toFa(totalQty);
    document.getElementById("regTotalPrice").textContent = money(totalPrice);
  }

  function renderAll(){
    renderChips(getFilteredServices());
    renderSelectedService();
    renderTodayItems();
    renderWorkerPage();
    renderApprovalPage();
    renderManagerPage();
    renderRegisterSummary();
  }

  tabs.forEach(btn => {
    btn.addEventListener("click", function(){
      const pageName = this.getAttribute("data-page");

      tabs.forEach(b => b.classList.remove("active"));
      this.classList.add("active");

      pages.forEach(page => page.classList.remove("active"));
      document.getElementById("page-" + pageName).classList.add("active");
    });
  });

  serviceSearch.addEventListener("input", function(){
    renderChips(getFilteredServices());
  });

  renderAll();
})();
</script>
برنامه کارگر۵۵
TEXT - 2026-05-05 21:40:18
<div class="erfan-app" dir="rtl"> <div class="erfan-phone"> <!-- Header --> <div class="erfan-header"> <div> <h2>ثبت کارکرد</h2> <p>کارگر: عرفان</p> </div> <div class="erfan-date">امروز</div> </div> <!-- Summary --> <div class="erfan-summary"> <div> <span>تعداد کل</span> <strong id="totalQty">۰</strong> </div> <div> <span>جمع مبلغ</span> <strong id="totalPrice">۰ تومان</strong> </div> </div> <!-- Search --> <div class="erfan-search-box"> <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..." autocomplete="off"> </div> <!-- Quick Services --> <div class="erfan-section-title">خدمات پرکاربرد</div> <div class="erfan-quick" id="quickServices"></div> <!-- Selected Service --> <div class="erfan-selected" id="selectedBox"> <div class="erfan-empty"> یک خدمت را انتخاب کن </div> </div> <!-- Today List --> <div class="erfan-section-title">ثبت‌های امروز</div> <div class="erfan-list" id="todayList"> <div class="erfan-noitem">هنوز چیزی ثبت نشده</div> </div> <!-- Bottom Submit --> <div class="erfan-bottom"> <button id="finalSubmit">ثبت نهایی امروز</button> </div> <div class="erfan-toast" id="toast">ثبت شد</div> </div> </div> <style> .erfan-app { width: 100%; min-height: 100vh; background: #eef1f5; font-family: Tahoma, Arial, sans-serif; display: flex; justify-content: center; padding: 0; box-sizing: border-box; } .erfan-phone { width: 100%; max-width: 430px; min-height: 100vh; background: #f8fafc; position: relative; overflow: hidden; padding: 18px 16px 96px; box-sizing: border-box; } .erfan-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; } .erfan-header h2 { margin: 0; font-size: 22px; color: #111827; font-weight: 800; } .erfan-header p { margin: 6px 0 0; font-size: 13px; color: #6b7280; } .erfan-date { background: #111827; color: #fff; font-size: 12px; padding: 8px 13px; border-radius: 999px; } .erfan-summary { background: linear-gradient(135deg, #111827, #1f2937); border-radius: 24px; padding: 18px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; color: #fff; box-shadow: 0 12px 30px rgba(17,24,39,.22); margin-bottom: 18px; } .erfan-summary div { background: rgba(255,255,255,.08); border-radius: 18px; padding: 14px 12px; } .erfan-summary span { display: block; font-size: 12px; color: #cbd5e1; margin-bottom: 7px; } .erfan-summary strong { display: block; font-size: 17px; font-weight: 800; color: #fff; } .erfan-search-box { margin-bottom: 14px; } .erfan-search-box input { width: 100%; height: 54px; border: none; outline: none; border-radius: 18px; padding: 0 18px; font-size: 15px; color: #111827; background: #fff; box-shadow: 0 8px 24px rgba(15,23,42,.07); box-sizing: border-box; } .erfan-search-box input::placeholder { color: #9ca3af; } .erfan-section-title { font-size: 13px; color: #6b7280; font-weight: 700; margin: 16px 2px 10px; } .erfan-quick { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; } .erfan-quick::-webkit-scrollbar { display: none; } .erfan-chip { flex: 0 0 auto; border: none; background: #fff; color: #111827; border-radius: 999px; padding: 12px 16px; font-size: 13px; font-weight: 700; box-shadow: 0 8px 20px rgba(15,23,42,.07); cursor: pointer; white-space: nowrap; } .erfan-chip.active { background: #2563eb; color: #fff; } .erfan-selected { margin-top: 14px; min-height: 120px; } .erfan-empty { background: #fff; border-radius: 22px; min-height: 110px; display: flex; align-items: center; justify-content: center; color: #9ca3af; font-size: 14px; box-shadow: 0 8px 24px rgba(15,23,42,.06); } .erfan-card { background: #fff; border-radius: 24px; padding: 16px; box-shadow: 0 10px 28px rgba(15,23,42,.08); } .erfan-card-top { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 14px; } .erfan-card h3 { margin: 0 0 7px; color: #111827; font-size: 17px; font-weight: 800; } .erfan-card p { margin: 0; color: #6b7280; font-size: 13px; } .erfan-price { background: #eff6ff; color: #2563eb; font-size: 13px; font-weight: 800; padding: 8px 10px; border-radius: 999px; white-space: nowrap; } .erfan-counter { display: grid; grid-template-columns: 54px 1fr 54px; gap: 10px; margin-bottom: 12px; } .erfan-counter button { height: 52px; border: none; border-radius: 17px; background: #f1f5f9; color: #111827; font-size: 24px; font-weight: 800; cursor: pointer; } .erfan-counter input { width: 100%; height: 52px; border: none; outline: none; border-radius: 17px; background: #f8fafc; text-align: center; font-size: 21px; font-weight: 800; color: #111827; } .erfan-add-btn { width: 100%; height: 54px; border: none; border-radius: 18px; background: #16a34a; color: #fff; font-size: 15px; font-weight: 800; cursor: pointer; } .erfan-list { display: flex; flex-direction: column; gap: 10px; } .erfan-noitem { background: transparent; color: #9ca3af; text-align: center; padding: 18px 0; font-size: 13px; } .erfan-row { background: #fff; border-radius: 18px; padding: 13px 14px; display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center; box-shadow: 0 7px 20px rgba(15,23,42,.055); } .erfan-row h4 { margin: 0 0 6px; color: #111827; font-size: 14px; font-weight: 800; } .erfan-row p { margin: 0; color: #6b7280; font-size: 12px; } .erfan-remove { width: 36px; height: 36px; border: none; border-radius: 13px; background: #fee2e2; color: #dc2626; font-size: 18px; font-weight: 800; cursor: pointer; } .erfan-bottom { position: fixed; bottom: 0; right: 50%; transform: translateX(50%); width: 100%; max-width: 430px; background: rgba(248,250,252,.92); backdrop-filter: blur(14px); padding: 14px 16px 18px; box-sizing: border-box; border-top: 1px solid rgba(226,232,240,.8); } .erfan-bottom button { width: 100%; height: 56px; border: none; border-radius: 20px; background: #2563eb; color: #fff; font-size: 16px; font-weight: 900; cursor: pointer; box-shadow: 0 12px 28px rgba(37,99,235,.3); } .erfan-toast { position: fixed; bottom: 90px; right: 50%; transform: translateX(50%) translateY(20px); background: #111827; color: #fff; padding: 12px 18px; border-radius: 999px; font-size: 13px; opacity: 0; pointer-events: none; transition: .25s ease; z-index: 9999; } .erfan-toast.show { opacity: 1; transform: translateX(50%) translateY(0); } @media (max-width: 360px) { .erfan-phone { padding: 15px 12px 94px; } .erfan-summary { padding: 14px; } .erfan-header h2 { font-size: 20px; } } </style> <script> (function() { const services = [ { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 }, { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 }, { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 }, { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 }, { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 }, { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 }, { id: 7, name: "میز لبه‌دار ۹۰", price: 170000 }, { id: 8, name: "میز لبه‌دار ۱۰۰", price: 190000 } ]; let selectedService = null; let items = []; let qty = 1; const quickServices = document.getElementById("quickServices"); const selectedBox = document.getElementById("selectedBox"); const todayList = document.getElementById("todayList"); const totalQty = document.getElementById("totalQty"); const totalPrice = document.getElementById("totalPrice"); const search = document.getElementById("serviceSearch"); const finalSubmit = document.getElementById("finalSubmit"); const toast = document.getElementById("toast"); function toFa(num) { return String(num).replace(/\d/g, d => "۰۱۲۳۴۵۶۷۸۹"[d]); } function money(num) { return toFa(num.toLocaleString("en-US")) + " تومان"; } function showToast(text) { toast.textContent = text; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1600); } function renderQuick(list = services) { quickServices.innerHTML = ""; list.forEach(service => { const btn = document.createElement("button"); btn.className = "erfan-chip" + (selectedService && selectedService.id === service.id ? " active" : ""); btn.textContent = service.name; btn.onclick = () => selectService(service.id); quickServices.appendChild(btn); }); } function selectService(id) { selectedService = services.find(s => s.id === id); qty = 1; renderQuick(getFilteredServices()); renderSelected(); } function renderSelected() { if (!selectedService) { selectedBox.innerHTML = `<div class="erfan-empty">یک خدمت را انتخاب کن</div>`; return; } selectedBox.innerHTML = ` <div class="erfan-card"> <div class="erfan-card-top"> <div> <h3>${selectedService.name}</h3> <p>قیمت واحد: ${money(selectedService.price)}</p> </div> <div class="erfan-price">${money(selectedService.price * qty)}</div> </div> <div class="erfan-counter"> <button type="button" id="minusBtn">−</button> <input id="qtyInput" type="number" min="1" value="${qty}"> <button type="button" id="plusBtn">+</button> </div> <button type="button" class="erfan-add-btn" id="addBtn">افزودن به امروز</button> </div> `; document.getElementById("minusBtn").onclick = () => { qty = Math.max(1, qty - 1); renderSelected(); }; document.getElementById("plusBtn").onclick = () => { qty++; renderSelected(); }; document.getElementById("qtyInput").oninput = (e) => { qty = Math.max(1, parseInt(e.target.value || "1")); renderSelected(); }; document.getElementById("addBtn").onclick = addItem; } function addItem() { if (!selectedService) return; const existing = items.find(i => i.id === selectedService.id); if (existing) { existing.qty += qty; } else { items.unshift({ id: selectedService.id, name: selectedService.name, price: selectedService.price, qty: qty }); } qty = 1; renderSelected(); renderItems(); showToast("به لیست امروز اضافه شد"); } function renderItems() { if (items.length === 0) { todayList.innerHTML = `<div class="erfan-noitem">هنوز چیزی ثبت نشده</div>`; } else { todayList.innerHTML = ""; items.forEach((item, index) => { const row = document.createElement("div"); row.className = "erfan-row"; row.innerHTML = ` <div> <h4>${item.name}</h4> <p>${toFa(item.qty)} عدد × ${money(item.price)} = ${money(item.qty * item.price)}</p> </div> <button class="erfan-remove" type="button">×</button> `; row.querySelector(".erfan-remove").onclick = () => { items.splice(index, 1); renderItems(); showToast("حذف شد"); }; todayList.appendChild(row); }); } updateSummary(); } function updateSummary() { const q = items.reduce((sum, item) => sum + item.qty, 0); const p = items.reduce((sum, item) => sum + item.qty * item.price, 0); totalQty.textContent = toFa(q); totalPrice.textContent = money(p); } function normalizeText(text) { return text .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)) .toLowerCase(); } function getFilteredServices() { const q = normalizeText(search.value.trim()); if (!q) return services; return services.filter(service => normalizeText(service.name).includes(q)); } search.addEventListener("input", () => { renderQuick(getFilteredServices()); }); finalSubmit.onclick = () => { if (items.length === 0) { showToast("چیزی برای ثبت وجود ندارد"); return; } showToast("ثبت نهایی انجام شد"); }; renderQuick(); renderSelected(); renderItems(); })(); </script>
<div class="erfan-app" dir="rtl">
  <div class="erfan-phone">

    <!-- Header -->
    <div class="erfan-header">
      <div>
        <h2>ثبت کارکرد</h2>
        <p>کارگر: عرفان</p>
      </div>
      <div class="erfan-date">امروز</div>
    </div>

    <!-- Summary -->
    <div class="erfan-summary">
      <div>
        <span>تعداد کل</span>
        <strong id="totalQty">۰</strong>
      </div>
      <div>
        <span>جمع مبلغ</span>
        <strong id="totalPrice">۰ تومان</strong>
      </div>
    </div>

    <!-- Search -->
    <div class="erfan-search-box">
      <input id="serviceSearch" type="text" placeholder="جستجوی سریع خدمت..." autocomplete="off">
    </div>

    <!-- Quick Services -->
    <div class="erfan-section-title">خدمات پرکاربرد</div>
    <div class="erfan-quick" id="quickServices"></div>

    <!-- Selected Service -->
    <div class="erfan-selected" id="selectedBox">
      <div class="erfan-empty">
        یک خدمت را انتخاب کن
      </div>
    </div>

    <!-- Today List -->
    <div class="erfan-section-title">ثبت‌های امروز</div>
    <div class="erfan-list" id="todayList">
      <div class="erfan-noitem">هنوز چیزی ثبت نشده</div>
    </div>

    <!-- Bottom Submit -->
    <div class="erfan-bottom">
      <button id="finalSubmit">ثبت نهایی امروز</button>
    </div>

    <div class="erfan-toast" id="toast">ثبت شد</div>

  </div>
</div>

<style>
  .erfan-app {
    width: 100%;
    min-height: 100vh;
    background: #eef1f5;
    font-family: Tahoma, Arial, sans-serif;
    display: flex;
    justify-content: center;
    padding: 0;
    box-sizing: border-box;
  }

  .erfan-phone {
    width: 100%;
    max-width: 430px;
    min-height: 100vh;
    background: #f8fafc;
    position: relative;
    overflow: hidden;
    padding: 18px 16px 96px;
    box-sizing: border-box;
  }

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

  .erfan-header h2 {
    margin: 0;
    font-size: 22px;
    color: #111827;
    font-weight: 800;
  }

  .erfan-header p {
    margin: 6px 0 0;
    font-size: 13px;
    color: #6b7280;
  }

  .erfan-date {
    background: #111827;
    color: #fff;
    font-size: 12px;
    padding: 8px 13px;
    border-radius: 999px;
  }

  .erfan-summary {
    background: linear-gradient(135deg, #111827, #1f2937);
    border-radius: 24px;
    padding: 18px;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    color: #fff;
    box-shadow: 0 12px 30px rgba(17,24,39,.22);
    margin-bottom: 18px;
  }

  .erfan-summary div {
    background: rgba(255,255,255,.08);
    border-radius: 18px;
    padding: 14px 12px;
  }

  .erfan-summary span {
    display: block;
    font-size: 12px;
    color: #cbd5e1;
    margin-bottom: 7px;
  }

  .erfan-summary strong {
    display: block;
    font-size: 17px;
    font-weight: 800;
    color: #fff;
  }

  .erfan-search-box {
    margin-bottom: 14px;
  }

  .erfan-search-box input {
    width: 100%;
    height: 54px;
    border: none;
    outline: none;
    border-radius: 18px;
    padding: 0 18px;
    font-size: 15px;
    color: #111827;
    background: #fff;
    box-shadow: 0 8px 24px rgba(15,23,42,.07);
    box-sizing: border-box;
  }

  .erfan-search-box input::placeholder {
    color: #9ca3af;
  }

  .erfan-section-title {
    font-size: 13px;
    color: #6b7280;
    font-weight: 700;
    margin: 16px 2px 10px;
  }

  .erfan-quick {
    display: flex;
    gap: 10px;
    overflow-x: auto;
    padding-bottom: 6px;
    scrollbar-width: none;
  }

  .erfan-quick::-webkit-scrollbar {
    display: none;
  }

  .erfan-chip {
    flex: 0 0 auto;
    border: none;
    background: #fff;
    color: #111827;
    border-radius: 999px;
    padding: 12px 16px;
    font-size: 13px;
    font-weight: 700;
    box-shadow: 0 8px 20px rgba(15,23,42,.07);
    cursor: pointer;
    white-space: nowrap;
  }

  .erfan-chip.active {
    background: #2563eb;
    color: #fff;
  }

  .erfan-selected {
    margin-top: 14px;
    min-height: 120px;
  }

  .erfan-empty {
    background: #fff;
    border-radius: 22px;
    min-height: 110px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #9ca3af;
    font-size: 14px;
    box-shadow: 0 8px 24px rgba(15,23,42,.06);
  }

  .erfan-card {
    background: #fff;
    border-radius: 24px;
    padding: 16px;
    box-shadow: 0 10px 28px rgba(15,23,42,.08);
  }

  .erfan-card-top {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    align-items: flex-start;
    margin-bottom: 14px;
  }

  .erfan-card h3 {
    margin: 0 0 7px;
    color: #111827;
    font-size: 17px;
    font-weight: 800;
  }

  .erfan-card p {
    margin: 0;
    color: #6b7280;
    font-size: 13px;
  }

  .erfan-price {
    background: #eff6ff;
    color: #2563eb;
    font-size: 13px;
    font-weight: 800;
    padding: 8px 10px;
    border-radius: 999px;
    white-space: nowrap;
  }

  .erfan-counter {
    display: grid;
    grid-template-columns: 54px 1fr 54px;
    gap: 10px;
    margin-bottom: 12px;
  }

  .erfan-counter button {
    height: 52px;
    border: none;
    border-radius: 17px;
    background: #f1f5f9;
    color: #111827;
    font-size: 24px;
    font-weight: 800;
    cursor: pointer;
  }

  .erfan-counter input {
    width: 100%;
    height: 52px;
    border: none;
    outline: none;
    border-radius: 17px;
    background: #f8fafc;
    text-align: center;
    font-size: 21px;
    font-weight: 800;
    color: #111827;
  }

  .erfan-add-btn {
    width: 100%;
    height: 54px;
    border: none;
    border-radius: 18px;
    background: #16a34a;
    color: #fff;
    font-size: 15px;
    font-weight: 800;
    cursor: pointer;
  }

  .erfan-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .erfan-noitem {
    background: transparent;
    color: #9ca3af;
    text-align: center;
    padding: 18px 0;
    font-size: 13px;
  }

  .erfan-row {
    background: #fff;
    border-radius: 18px;
    padding: 13px 14px;
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 10px;
    align-items: center;
    box-shadow: 0 7px 20px rgba(15,23,42,.055);
  }

  .erfan-row h4 {
    margin: 0 0 6px;
    color: #111827;
    font-size: 14px;
    font-weight: 800;
  }

  .erfan-row p {
    margin: 0;
    color: #6b7280;
    font-size: 12px;
  }

  .erfan-remove {
    width: 36px;
    height: 36px;
    border: none;
    border-radius: 13px;
    background: #fee2e2;
    color: #dc2626;
    font-size: 18px;
    font-weight: 800;
    cursor: pointer;
  }

  .erfan-bottom {
    position: fixed;
    bottom: 0;
    right: 50%;
    transform: translateX(50%);
    width: 100%;
    max-width: 430px;
    background: rgba(248,250,252,.92);
    backdrop-filter: blur(14px);
    padding: 14px 16px 18px;
    box-sizing: border-box;
    border-top: 1px solid rgba(226,232,240,.8);
  }

  .erfan-bottom button {
    width: 100%;
    height: 56px;
    border: none;
    border-radius: 20px;
    background: #2563eb;
    color: #fff;
    font-size: 16px;
    font-weight: 900;
    cursor: pointer;
    box-shadow: 0 12px 28px rgba(37,99,235,.3);
  }

  .erfan-toast {
    position: fixed;
    bottom: 90px;
    right: 50%;
    transform: translateX(50%) translateY(20px);
    background: #111827;
    color: #fff;
    padding: 12px 18px;
    border-radius: 999px;
    font-size: 13px;
    opacity: 0;
    pointer-events: none;
    transition: .25s ease;
    z-index: 9999;
  }

  .erfan-toast.show {
    opacity: 1;
    transform: translateX(50%) translateY(0);
  }

  @media (max-width: 360px) {
    .erfan-phone {
      padding: 15px 12px 94px;
    }

    .erfan-summary {
      padding: 14px;
    }

    .erfan-header h2 {
      font-size: 20px;
    }
  }
</style>

<script>
(function() {
  const services = [
    { id: 1, name: "میز لبه‌دار ۳۵", price: 95000 },
    { id: 2, name: "میز لبه‌دار ۴۲", price: 102000 },
    { id: 3, name: "میز لبه‌دار ۵۰", price: 110000 },
    { id: 4, name: "میز لبه‌دار ۶۰", price: 125000 },
    { id: 5, name: "میز لبه‌دار ۷۰", price: 140000 },
    { id: 6, name: "میز لبه‌دار ۸۰", price: 155000 },
    { id: 7, name: "میز لبه‌دار ۹۰", price: 170000 },
    { id: 8, name: "میز لبه‌دار ۱۰۰", price: 190000 }
  ];

  let selectedService = null;
  let items = [];
  let qty = 1;

  const quickServices = document.getElementById("quickServices");
  const selectedBox = document.getElementById("selectedBox");
  const todayList = document.getElementById("todayList");
  const totalQty = document.getElementById("totalQty");
  const totalPrice = document.getElementById("totalPrice");
  const search = document.getElementById("serviceSearch");
  const finalSubmit = document.getElementById("finalSubmit");
  const toast = document.getElementById("toast");

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

  function money(num) {
    return toFa(num.toLocaleString("en-US")) + " تومان";
  }

  function showToast(text) {
    toast.textContent = text;
    toast.classList.add("show");
    setTimeout(() => toast.classList.remove("show"), 1600);
  }

  function renderQuick(list = services) {
    quickServices.innerHTML = "";

    list.forEach(service => {
      const btn = document.createElement("button");
      btn.className = "erfan-chip" + (selectedService && selectedService.id === service.id ? " active" : "");
      btn.textContent = service.name;
      btn.onclick = () => selectService(service.id);
      quickServices.appendChild(btn);
    });
  }

  function selectService(id) {
    selectedService = services.find(s => s.id === id);
    qty = 1;
    renderQuick(getFilteredServices());
    renderSelected();
  }

  function renderSelected() {
    if (!selectedService) {
      selectedBox.innerHTML = `<div class="erfan-empty">یک خدمت را انتخاب کن</div>`;
      return;
    }

    selectedBox.innerHTML = `
      <div class="erfan-card">
        <div class="erfan-card-top">
          <div>
            <h3>${selectedService.name}</h3>
            <p>قیمت واحد: ${money(selectedService.price)}</p>
          </div>
          <div class="erfan-price">${money(selectedService.price * qty)}</div>
        </div>

        <div class="erfan-counter">
          <button type="button" id="minusBtn">−</button>
          <input id="qtyInput" type="number" min="1" value="${qty}">
          <button type="button" id="plusBtn">+</button>
        </div>

        <button type="button" class="erfan-add-btn" id="addBtn">افزودن به امروز</button>
      </div>
    `;

    document.getElementById("minusBtn").onclick = () => {
      qty = Math.max(1, qty - 1);
      renderSelected();
    };

    document.getElementById("plusBtn").onclick = () => {
      qty++;
      renderSelected();
    };

    document.getElementById("qtyInput").oninput = (e) => {
      qty = Math.max(1, parseInt(e.target.value || "1"));
      renderSelected();
    };

    document.getElementById("addBtn").onclick = addItem;
  }

  function addItem() {
    if (!selectedService) return;

    const existing = items.find(i => i.id === selectedService.id);

    if (existing) {
      existing.qty += qty;
    } else {
      items.unshift({
        id: selectedService.id,
        name: selectedService.name,
        price: selectedService.price,
        qty: qty
      });
    }

    qty = 1;
    renderSelected();
    renderItems();
    showToast("به لیست امروز اضافه شد");
  }

  function renderItems() {
    if (items.length === 0) {
      todayList.innerHTML = `<div class="erfan-noitem">هنوز چیزی ثبت نشده</div>`;
    } else {
      todayList.innerHTML = "";

      items.forEach((item, index) => {
        const row = document.createElement("div");
        row.className = "erfan-row";
        row.innerHTML = `
          <div>
            <h4>${item.name}</h4>
            <p>${toFa(item.qty)} عدد × ${money(item.price)} = ${money(item.qty * item.price)}</p>
          </div>
          <button class="erfan-remove" type="button">×</button>
        `;

        row.querySelector(".erfan-remove").onclick = () => {
          items.splice(index, 1);
          renderItems();
          showToast("حذف شد");
        };

        todayList.appendChild(row);
      });
    }

    updateSummary();
  }

  function updateSummary() {
    const q = items.reduce((sum, item) => sum + item.qty, 0);
    const p = items.reduce((sum, item) => sum + item.qty * item.price, 0);

    totalQty.textContent = toFa(q);
    totalPrice.textContent = money(p);
  }

  function normalizeText(text) {
    return text
      .replace(/ي/g, "ی")
      .replace(/ك/g, "ک")
      .replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
      .toLowerCase();
  }

  function getFilteredServices() {
    const q = normalizeText(search.value.trim());
    if (!q) return services;

    return services.filter(service => normalizeText(service.name).includes(q));
  }

  search.addEventListener("input", () => {
    renderQuick(getFilteredServices());
  });

  finalSubmit.onclick = () => {
    if (items.length === 0) {
      showToast("چیزی برای ثبت وجود ندارد");
      return;
    }

    showToast("ثبت نهایی انجام شد");
  };

  renderQuick();
  renderSelected();
  renderItems();
})();
</script>
کد ۱۶ عالی
TEXT - 2026-05-05 21:18:45
<?php if (!defined('ABSPATH')) exit; /* ========================= * دسترسی * ========================= */ function mvx_user_can_manage() { return is_user_logged_in() && ( current_user_can('manage_options') || current_user_can('manage_woocommerce') || current_user_can('edit_products') ); } /* ========================= * گرفتن محصول جاری * ========================= */ function mvx_get_current_product() { if (!function_exists('is_product') || !is_product()) return false; $product_id = get_queried_object_id(); if (!$product_id) return false; $product = wc_get_product($product_id); return ($product && is_a($product, 'WC_Product')) ? $product : false; } /* ========================= * همه attribute ها برای فرم * - global => value = term_id * - local => value = خود متن * ========================= */ function mvx_get_all_attributes_for_form($product = false) { $result = array(); /* global attributes */ if (function_exists('wc_get_attribute_taxonomies')) { $taxonomies = wc_get_attribute_taxonomies(); if (!empty($taxonomies)) { foreach ($taxonomies as $tax) { $taxonomy_name = wc_attribute_taxonomy_name($tax->attribute_name); // pa_color if (!taxonomy_exists($taxonomy_name)) continue; $terms = get_terms(array( 'taxonomy' => $taxonomy_name, 'hide_empty' => false, )); $options = array(); if (!is_wp_error($terms) && !empty($terms)) { foreach ($terms as $term) { $options[] = array( 'value' => (string) $term->term_id, // مهم: term_id 'label' => $term->name, 'term_id' => (int) $term->term_id, 'slug' => $term->slug, 'name' => $term->name, ); } } $result[$taxonomy_name] = array( 'name' => $taxonomy_name, 'label' => $tax->attribute_label ? $tax->attribute_label : $tax->attribute_name, 'is_taxonomy' => true, 'options' => $options, 'source' => 'global', ); } } } /* local product attributes */ if ($product) { $product_attributes = $product->get_attributes(); if (!empty($product_attributes)) { foreach ($product_attributes as $key => $attribute) { if (!is_a($attribute, 'WC_Product_Attribute')) continue; if ($attribute->is_taxonomy()) continue; $name = $attribute->get_name(); $label = wc_attribute_label($name, $product); $options = array(); foreach ((array) $attribute->get_options() as $opt) { $opt = (string) $opt; $options[] = array( 'value' => $opt, 'label' => $opt, ); } $result[$name] = array( 'name' => $name, 'label' => $label ? $label : $name, 'is_taxonomy' => false, 'options' => $options, 'source' => 'local', ); } } } return $result; } /* ========================= * پیدا کردن تعریف attribute * ========================= */ function mvx_find_attribute_definition($all_attrs, $attr_name) { return isset($all_attrs[$attr_name]) ? $all_attrs[$attr_name] : false; } /* ========================= * پیدا کردن term از روی term_id * ========================= */ function mvx_get_term_from_posted_value($taxonomy, $posted_value) { $term_id = absint($posted_value); if (!$term_id) return false; $term = get_term($term_id, $taxonomy); if ($term && !is_wp_error($term)) { return $term; } return false; } /* ========================= * افزودن option به attribute محصول * ========================= */ function mvx_add_option_to_product_attribute($product_id, $attr_name, $posted_value, $attr_def = false) { $product = wc_get_product($product_id); if (!$product) return false; $attributes = $product->get_attributes(); $found = false; foreach ($attributes as $key => $attribute) { if (!is_a($attribute, 'WC_Product_Attribute')) continue; if ($attribute->get_name() !== $attr_name) continue; $found = true; if ($attribute->is_taxonomy()) { $term = false; if ($attr_def && !empty($attr_def['is_taxonomy'])) { $term = mvx_get_term_from_posted_value($attr_name, $posted_value); } if ($term) { wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true); } $attribute->set_visible(true); $attribute->set_variation(true); $attributes[$key] = $attribute; } else { $attr_value = (string) $posted_value; $options = (array) $attribute->get_options(); if (!in_array($attr_value, $options, true)) { $options[] = $attr_value; $attribute->set_options($options); } $attribute->set_visible(true); $attribute->set_variation(true); $attributes[$key] = $attribute; } } if (!$found) { $new_attr = new WC_Product_Attribute(); $new_attr->set_name($attr_name); $new_attr->set_visible(true); $new_attr->set_variation(true); $new_attr->set_position(count($attributes)); if ($attr_def && !empty($attr_def['is_taxonomy']) && taxonomy_exists($attr_name)) { $term = mvx_get_term_from_posted_value($attr_name, $posted_value); if ($term) { wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true); } $new_attr->set_id(wc_attribute_taxonomy_id_by_name($attr_name)); $new_attr->set_options(array()); } else { $attr_value = (string) $posted_value; $new_attr->set_options(array($attr_value)); } $attributes[$attr_name] = $new_attr; } $product->set_attributes($attributes); $product->save(); return true; } /* ========================= * آماده‌سازی مقدار variation * global => slug term * local => text * ========================= */ function mvx_prepare_variation_value($attr_def, $raw_value) { if (!$attr_def) { return array( 'meta_value' => $raw_value, 'variation_value' => $raw_value, 'display_value' => $raw_value, ); } if (!empty($attr_def['is_taxonomy'])) { $taxonomy = $attr_def['name']; $term = mvx_get_term_from_posted_value($taxonomy, $raw_value); if ($term) { return array( 'meta_value' => $term->slug, 'variation_value' => $term->slug, 'display_value' => $term->name, ); } return array( 'meta_value' => '', 'variation_value' => '', 'display_value' => '', ); } return array( 'meta_value' => (string) $raw_value, 'variation_value' => (string) $raw_value, 'display_value' => (string) $raw_value, ); } /* ========================= * آیا variation وجود دارد؟ * ========================= */ function mvx_variation_exists($product_id, $attrs_meta) { $children = get_posts(array( 'post_parent' => $product_id, 'post_type' => 'product_variation', 'post_status' => array('publish', 'private'), 'numberposts' => -1, 'fields' => 'ids', )); foreach ($children as $variation_id) { $matched = true; foreach ($attrs_meta as $meta_key => $meta_value) { $saved = get_post_meta($variation_id, $meta_key, true); if ((string) $saved !== (string) $meta_value) { $matched = false; break; } } if ($matched) return true; } return false; } /* ========================= * simple -> variable * ========================= */ function mvx_convert_simple_to_variable($product_id) { wp_set_object_terms($product_id, 'variable', 'product_type'); wc_delete_product_transients($product_id); return wc_get_product($product_id); } /* ========================= * پردازش فرم * ========================= */ function mvx_handle_form_submit() { if (is_admin()) return; if (!mvx_user_can_manage()) return; if (empty($_POST['mvx_action']) || $_POST['mvx_action'] !== 'add_variation_two_rows') return; if (empty($_POST['mvx_nonce']) || !wp_verify_nonce($_POST['mvx_nonce'], 'mvx_add_variation_two_rows')) { return; } $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0; $attr1 = isset($_POST['mvx_attr1']) ? wc_clean(wp_unslash($_POST['mvx_attr1'])) : ''; $val1 = isset($_POST['mvx_val1']) ? wp_unslash($_POST['mvx_val1']) : ''; $attr2 = isset($_POST['mvx_attr2']) ? wc_clean(wp_unslash($_POST['mvx_attr2'])) : ''; $val2 = isset($_POST['mvx_val2']) ? wp_unslash($_POST['mvx_val2']) : ''; $price = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : ''; if (!$product_id || !$attr1 || $val1 === '') { wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id))); exit; } if ($attr1 && $attr2 && $attr1 === $attr2) { wp_safe_redirect(add_query_arg('mvx_msg', 'sameattr', get_permalink($product_id))); exit; } $product = wc_get_product($product_id); if (!$product) return; if ($product->get_type() === 'simple') { $product = mvx_convert_simple_to_variable($product_id); } if (!$product || $product->get_type() !== 'variable') { wp_safe_redirect(add_query_arg('mvx_msg', 'notvariable', get_permalink($product_id))); exit; } $all_attrs = mvx_get_all_attributes_for_form($product); $def1 = mvx_find_attribute_definition($all_attrs, $attr1); $def2 = $attr2 ? mvx_find_attribute_definition($all_attrs, $attr2) : false; if (!$def1) { wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id))); exit; } if ($attr2 && !$def2) { wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id))); exit; } mvx_add_option_to_product_attribute($product_id, $attr1, $val1, $def1); if ($attr2 && $val2 !== '') { mvx_add_option_to_product_attribute($product_id, $attr2, $val2, $def2); } $prepared1 = mvx_prepare_variation_value($def1, $val1); if ($prepared1['meta_value'] === '') { wp_safe_redirect(add_query_arg('mvx_msg', 'badvalue', get_permalink($product_id))); exit; } $variation_meta = array( 'attribute_' . $attr1 => $prepared1['meta_value'], ); $variation_set_attrs = array( $attr1 => $prepared1['variation_value'], ); if ($attr2 && $val2 !== '') { $prepared2 = mvx_prepare_variation_value($def2, $val2); if ($prepared2['meta_value'] === '') { wp_safe_redirect(add_query_arg('mvx_msg', 'badvalue', get_permalink($product_id))); exit; } $variation_meta['attribute_' . $attr2] = $prepared2['meta_value']; $variation_set_attrs[$attr2] = $prepared2['variation_value']; } if (mvx_variation_exists($product_id, $variation_meta)) { wp_safe_redirect(add_query_arg('mvx_msg', 'exists', get_permalink($product_id))); exit; } $variation = new WC_Product_Variation(); $variation->set_parent_id($product_id); $variation->set_status('publish'); $variation->set_attributes($variation_set_attrs); if ($price !== '') { $variation->set_regular_price($price); $variation->set_price($price); } $variation_id = $variation->save(); if (!$variation_id || is_wp_error($variation_id)) { wp_safe_redirect(add_query_arg('mvx_msg', 'error', get_permalink($product_id))); exit; } foreach ($variation_meta as $meta_key => $meta_value) { update_post_meta($variation_id, $meta_key, $meta_value); } update_post_meta($variation_id, '_virtual', 'no'); update_post_meta($variation_id, '_downloadable', 'no'); WC_Product_Variable::sync($product_id); wc_delete_product_transients($product_id); wp_safe_redirect(add_query_arg(array( 'mvx_msg' => 'created', 'mvx_vid' => $variation_id, ), get_permalink($product_id))); exit; } add_action('template_redirect', 'mvx_handle_form_submit'); /* ========================= * UI * ========================= */ function mvx_render_variation_box() { if (is_admin()) return; if (!function_exists('is_product') || !is_product()) return; if (!mvx_user_can_manage()) return; $product = mvx_get_current_product(); if (!$product) return; $attrs_assoc = mvx_get_all_attributes_for_form($product); $attrs = array_values($attrs_assoc); echo '<div id="mvx-box" style="position:fixed;left:20px;bottom:20px;width:560px;max-width:calc(100vw - 30px);z-index:999999;background:#fff;border:2px solid #2563eb;border-radius:14px;box-shadow:0 12px 35px rgba(0,0,0,.18);padding:14px;direction:rtl;text-align:right;">'; echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">'; echo '<strong style="font-size:15px;">افزودن تنوع</strong>'; echo '<button type="button" onclick="document.getElementById(\'mvx-box\').style.display=\'none\';" style="background:#e5e7eb;border:none;border-radius:8px;padding:2px 8px;cursor:pointer;">×</button>'; echo '</div>'; if (isset($_GET['mvx_msg'])) { $msg = sanitize_text_field(wp_unslash($_GET['mvx_msg'])); $style_ok = 'background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;padding:8px 10px;border-radius:10px;margin-bottom:10px;'; $style_bad = 'background:#fef2f2;color:#991b1b;border:1px solid #fecaca;padding:8px 10px;border-radius:10px;margin-bottom:10px;'; $style_wrn = 'background:#fff7ed;color:#9a3412;border:1px solid #fdba74;padding:8px 10px;border-radius:10px;margin-bottom:10px;'; if ($msg === 'created') { echo '<div style="'.$style_ok.'">تنوع ساخته شد.</div>'; } elseif ($msg === 'exists') { echo '<div style="'.$style_wrn.'">این ترکیب از قبل وجود دارد.</div>'; } elseif ($msg === 'sameattr') { echo '<div style="'.$style_bad.'">ویژگی ردیف اول و دوم نباید یکی باشد.</div>'; } elseif ($msg === 'missing') { echo '<div style="'.$style_bad.'">ویژگی و مقدار معتبر الزامی است.</div>'; } elseif ($msg === 'notvariable') { echo '<div style="'.$style_bad.'">محصول به variable تبدیل نشد.</div>'; } elseif ($msg === 'badvalue') { echo '<div style="'.$style_bad.'">مقدار انتخابی معتبر نیست.</div>'; } elseif ($msg === 'error') { echo '<div style="'.$style_bad.'">خطا در ساخت variation.</div>'; } } echo '<form method="post">'; echo wp_nonce_field('mvx_add_variation_two_rows', 'mvx_nonce', true, false); echo '<input type="hidden" name="mvx_action" value="add_variation_two_rows">'; echo '<input type="hidden" name="mvx_product_id" value="' . esc_attr($product->get_id()) . '">'; echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;">'; echo '<div>'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">ویژگی</label>'; echo '<select id="mvx_attr1" name="mvx_attr1" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '<option value="">انتخاب ویژگی</option>'; foreach ($attrs as $a) { echo '<option value="' . esc_attr($a['name']) . '">' . esc_html($a['label']) . '</option>'; } echo '</select>'; echo '</div>'; echo '<div>'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">مقدار</label>'; echo '<select id="mvx_val1" name="mvx_val1" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '<option value="">ابتدا ویژگی را انتخاب کنید</option>'; echo '</select>'; echo '</div>'; echo '</div>'; echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;">'; echo '<div>'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">ویژگی</label>'; echo '<select id="mvx_attr2" name="mvx_attr2" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '<option value="">انتخاب ویژگی</option>'; foreach ($attrs as $a) { echo '<option value="' . esc_attr($a['name']) . '">' . esc_html($a['label']) . '</option>'; } echo '</select>'; echo '</div>'; echo '<div>'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">مقدار</label>'; echo '<select id="mvx_val2" name="mvx_val2" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '<option value="">ابتدا ویژگی را انتخاب کنید</option>'; echo '</select>'; echo '</div>'; echo '</div>'; echo '<div style="margin-bottom:12px;">'; echo '<label style="display:block;margin-bottom:5px;font-weight:700;">قیمت</label>'; echo '<input type="number" step="any" name="mvx_price" placeholder="مثلاً 350000" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">'; echo '</div>'; echo '<button type="submit" onclick="return confirm(\'تنوع اضافه شود؟\');" style="width:100%;background:#2563eb;color:#fff;border:none;border-radius:10px;padding:12px;cursor:pointer;font-weight:700;">افزودن تنوع</button>'; echo '</form>'; $map = array(); foreach ($attrs as $a) { $map[$a['name']] = $a['options']; } echo '<script>'; echo 'window.mvxAttrMap = ' . wp_json_encode($map) . ';'; ?> (function(){ function bind(attrId, valId, otherAttrId){ var attr = document.getElementById(attrId); var val = document.getElementById(valId); var other = document.getElementById(otherAttrId); if(!attr || !val) return; function refill(){ val.innerHTML = ''; var selected = attr.value; if(other && other.value && selected && other.value === selected){ alert('این ویژگی در ردیف دیگر انتخاب شده است.'); attr.value = ''; selected = ''; } if(!selected){ var op = document.createElement('option'); op.value = ''; op.textContent = 'ابتدا ویژگی را انتخاب کنید'; val.appendChild(op); return; } var items = (window.mvxAttrMap && window.mvxAttrMap[selected]) ? window.mvxAttrMap[selected] : []; if(!items.length){ var op2 = document.createElement('option'); op2.value = ''; op2.textContent = 'مقداری یافت نشد'; val.appendChild(op2); return; } var first = document.createElement('option'); first.value = ''; first.textContent = 'انتخاب مقدار'; val.appendChild(first); items.forEach(function(item){ var op3 = document.createElement('option'); op3.value = item.value; op3.textContent = item.label; val.appendChild(op3); }); } attr.addEventListener('change', refill); refill(); } bind('mvx_attr1', 'mvx_val1', 'mvx_attr2'); bind('mvx_attr2', 'mvx_val2', 'mvx_attr1'); })(); <?php echo '</script>'; echo '</div>'; } add_action('wp_footer', 'mvx_render_variation_box', 99);
<?php
if (!defined('ABSPATH')) exit;

/* =========================
 * دسترسی
 * ========================= */
function mvx_user_can_manage() {
    return is_user_logged_in() && (
        current_user_can('manage_options') ||
        current_user_can('manage_woocommerce') ||
        current_user_can('edit_products')
    );
}

/* =========================
 * گرفتن محصول جاری
 * ========================= */
function mvx_get_current_product() {
    if (!function_exists('is_product') || !is_product()) return false;

    $product_id = get_queried_object_id();
    if (!$product_id) return false;

    $product = wc_get_product($product_id);
    return ($product && is_a($product, 'WC_Product')) ? $product : false;
}

/* =========================
 * همه attribute ها برای فرم
 * - global => value = term_id
 * - local  => value = خود متن
 * ========================= */
function mvx_get_all_attributes_for_form($product = false) {
    $result = array();

    /* global attributes */
    if (function_exists('wc_get_attribute_taxonomies')) {
        $taxonomies = wc_get_attribute_taxonomies();

        if (!empty($taxonomies)) {
            foreach ($taxonomies as $tax) {
                $taxonomy_name = wc_attribute_taxonomy_name($tax->attribute_name); // pa_color
                if (!taxonomy_exists($taxonomy_name)) continue;

                $terms = get_terms(array(
                    'taxonomy'   => $taxonomy_name,
                    'hide_empty' => false,
                ));

                $options = array();
                if (!is_wp_error($terms) && !empty($terms)) {
                    foreach ($terms as $term) {
                        $options[] = array(
                            'value'   => (string) $term->term_id, // مهم: term_id
                            'label'   => $term->name,
                            'term_id' => (int) $term->term_id,
                            'slug'    => $term->slug,
                            'name'    => $term->name,
                        );
                    }
                }

                $result[$taxonomy_name] = array(
                    'name'        => $taxonomy_name,
                    'label'       => $tax->attribute_label ? $tax->attribute_label : $tax->attribute_name,
                    'is_taxonomy' => true,
                    'options'     => $options,
                    'source'      => 'global',
                );
            }
        }
    }

    /* local product attributes */
    if ($product) {
        $product_attributes = $product->get_attributes();

        if (!empty($product_attributes)) {
            foreach ($product_attributes as $key => $attribute) {
                if (!is_a($attribute, 'WC_Product_Attribute')) continue;
                if ($attribute->is_taxonomy()) continue;

                $name  = $attribute->get_name();
                $label = wc_attribute_label($name, $product);

                $options = array();
                foreach ((array) $attribute->get_options() as $opt) {
                    $opt = (string) $opt;
                    $options[] = array(
                        'value' => $opt,
                        'label' => $opt,
                    );
                }

                $result[$name] = array(
                    'name'        => $name,
                    'label'       => $label ? $label : $name,
                    'is_taxonomy' => false,
                    'options'     => $options,
                    'source'      => 'local',
                );
            }
        }
    }

    return $result;
}

/* =========================
 * پیدا کردن تعریف attribute
 * ========================= */
function mvx_find_attribute_definition($all_attrs, $attr_name) {
    return isset($all_attrs[$attr_name]) ? $all_attrs[$attr_name] : false;
}

/* =========================
 * پیدا کردن term از روی term_id
 * ========================= */
function mvx_get_term_from_posted_value($taxonomy, $posted_value) {
    $term_id = absint($posted_value);
    if (!$term_id) return false;

    $term = get_term($term_id, $taxonomy);
    if ($term && !is_wp_error($term)) {
        return $term;
    }

    return false;
}

/* =========================
 * افزودن option به attribute محصول
 * ========================= */
function mvx_add_option_to_product_attribute($product_id, $attr_name, $posted_value, $attr_def = false) {
    $product = wc_get_product($product_id);
    if (!$product) return false;

    $attributes = $product->get_attributes();
    $found = false;

    foreach ($attributes as $key => $attribute) {
        if (!is_a($attribute, 'WC_Product_Attribute')) continue;
        if ($attribute->get_name() !== $attr_name) continue;

        $found = true;

        if ($attribute->is_taxonomy()) {
            $term = false;

            if ($attr_def && !empty($attr_def['is_taxonomy'])) {
                $term = mvx_get_term_from_posted_value($attr_name, $posted_value);
            }

            if ($term) {
                wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true);
            }

            $attribute->set_visible(true);
            $attribute->set_variation(true);
            $attributes[$key] = $attribute;

        } else {
            $attr_value = (string) $posted_value;
            $options = (array) $attribute->get_options();

            if (!in_array($attr_value, $options, true)) {
                $options[] = $attr_value;
                $attribute->set_options($options);
            }

            $attribute->set_visible(true);
            $attribute->set_variation(true);
            $attributes[$key] = $attribute;
        }
    }

    if (!$found) {
        $new_attr = new WC_Product_Attribute();
        $new_attr->set_name($attr_name);
        $new_attr->set_visible(true);
        $new_attr->set_variation(true);
        $new_attr->set_position(count($attributes));

        if ($attr_def && !empty($attr_def['is_taxonomy']) && taxonomy_exists($attr_name)) {
            $term = mvx_get_term_from_posted_value($attr_name, $posted_value);

            if ($term) {
                wp_set_object_terms($product_id, array((int) $term->term_id), $attr_name, true);
            }

            $new_attr->set_id(wc_attribute_taxonomy_id_by_name($attr_name));
            $new_attr->set_options(array());
        } else {
            $attr_value = (string) $posted_value;
            $new_attr->set_options(array($attr_value));
        }

        $attributes[$attr_name] = $new_attr;
    }

    $product->set_attributes($attributes);
    $product->save();

    return true;
}

/* =========================
 * آماده‌سازی مقدار variation
 * global => slug term
 * local  => text
 * ========================= */
function mvx_prepare_variation_value($attr_def, $raw_value) {
    if (!$attr_def) {
        return array(
            'meta_value'      => $raw_value,
            'variation_value' => $raw_value,
            'display_value'   => $raw_value,
        );
    }

    if (!empty($attr_def['is_taxonomy'])) {
        $taxonomy = $attr_def['name'];
        $term = mvx_get_term_from_posted_value($taxonomy, $raw_value);

        if ($term) {
            return array(
                'meta_value'      => $term->slug,
                'variation_value' => $term->slug,
                'display_value'   => $term->name,
            );
        }

        return array(
            'meta_value'      => '',
            'variation_value' => '',
            'display_value'   => '',
        );
    }

    return array(
        'meta_value'      => (string) $raw_value,
        'variation_value' => (string) $raw_value,
        'display_value'   => (string) $raw_value,
    );
}

/* =========================
 * آیا variation وجود دارد؟
 * ========================= */
function mvx_variation_exists($product_id, $attrs_meta) {
    $children = get_posts(array(
        'post_parent' => $product_id,
        'post_type'   => 'product_variation',
        'post_status' => array('publish', 'private'),
        'numberposts' => -1,
        'fields'      => 'ids',
    ));

    foreach ($children as $variation_id) {
        $matched = true;

        foreach ($attrs_meta as $meta_key => $meta_value) {
            $saved = get_post_meta($variation_id, $meta_key, true);
            if ((string) $saved !== (string) $meta_value) {
                $matched = false;
                break;
            }
        }

        if ($matched) return true;
    }

    return false;
}

/* =========================
 * simple -> variable
 * ========================= */
function mvx_convert_simple_to_variable($product_id) {
    wp_set_object_terms($product_id, 'variable', 'product_type');
    wc_delete_product_transients($product_id);
    return wc_get_product($product_id);
}

/* =========================
 * پردازش فرم
 * ========================= */
function mvx_handle_form_submit() {
    if (is_admin()) return;
    if (!mvx_user_can_manage()) return;

    if (empty($_POST['mvx_action']) || $_POST['mvx_action'] !== 'add_variation_two_rows') return;

    if (empty($_POST['mvx_nonce']) || !wp_verify_nonce($_POST['mvx_nonce'], 'mvx_add_variation_two_rows')) {
        return;
    }

    $product_id = isset($_POST['mvx_product_id']) ? absint($_POST['mvx_product_id']) : 0;

    $attr1 = isset($_POST['mvx_attr1']) ? wc_clean(wp_unslash($_POST['mvx_attr1'])) : '';
    $val1  = isset($_POST['mvx_val1']) ? wp_unslash($_POST['mvx_val1']) : '';

    $attr2 = isset($_POST['mvx_attr2']) ? wc_clean(wp_unslash($_POST['mvx_attr2'])) : '';
    $val2  = isset($_POST['mvx_val2']) ? wp_unslash($_POST['mvx_val2']) : '';

    $price = isset($_POST['mvx_price']) ? wc_format_decimal(wp_unslash($_POST['mvx_price'])) : '';

    if (!$product_id || !$attr1 || $val1 === '') {
        wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id)));
        exit;
    }

    if ($attr1 && $attr2 && $attr1 === $attr2) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'sameattr', get_permalink($product_id)));
        exit;
    }

    $product = wc_get_product($product_id);
    if (!$product) return;

    if ($product->get_type() === 'simple') {
        $product = mvx_convert_simple_to_variable($product_id);
    }

    if (!$product || $product->get_type() !== 'variable') {
        wp_safe_redirect(add_query_arg('mvx_msg', 'notvariable', get_permalink($product_id)));
        exit;
    }

    $all_attrs = mvx_get_all_attributes_for_form($product);

    $def1 = mvx_find_attribute_definition($all_attrs, $attr1);
    $def2 = $attr2 ? mvx_find_attribute_definition($all_attrs, $attr2) : false;

    if (!$def1) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id)));
        exit;
    }

    if ($attr2 && !$def2) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'missing', get_permalink($product_id)));
        exit;
    }

    mvx_add_option_to_product_attribute($product_id, $attr1, $val1, $def1);
    if ($attr2 && $val2 !== '') {
        mvx_add_option_to_product_attribute($product_id, $attr2, $val2, $def2);
    }

    $prepared1 = mvx_prepare_variation_value($def1, $val1);

    if ($prepared1['meta_value'] === '') {
        wp_safe_redirect(add_query_arg('mvx_msg', 'badvalue', get_permalink($product_id)));
        exit;
    }

    $variation_meta = array(
        'attribute_' . $attr1 => $prepared1['meta_value'],
    );

    $variation_set_attrs = array(
        $attr1 => $prepared1['variation_value'],
    );

    if ($attr2 && $val2 !== '') {
        $prepared2 = mvx_prepare_variation_value($def2, $val2);

        if ($prepared2['meta_value'] === '') {
            wp_safe_redirect(add_query_arg('mvx_msg', 'badvalue', get_permalink($product_id)));
            exit;
        }

        $variation_meta['attribute_' . $attr2] = $prepared2['meta_value'];
        $variation_set_attrs[$attr2] = $prepared2['variation_value'];
    }

    if (mvx_variation_exists($product_id, $variation_meta)) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'exists', get_permalink($product_id)));
        exit;
    }

    $variation = new WC_Product_Variation();
    $variation->set_parent_id($product_id);
    $variation->set_status('publish');
    $variation->set_attributes($variation_set_attrs);

    if ($price !== '') {
        $variation->set_regular_price($price);
        $variation->set_price($price);
    }

    $variation_id = $variation->save();

    if (!$variation_id || is_wp_error($variation_id)) {
        wp_safe_redirect(add_query_arg('mvx_msg', 'error', get_permalink($product_id)));
        exit;
    }

    foreach ($variation_meta as $meta_key => $meta_value) {
        update_post_meta($variation_id, $meta_key, $meta_value);
    }

    update_post_meta($variation_id, '_virtual', 'no');
    update_post_meta($variation_id, '_downloadable', 'no');

    WC_Product_Variable::sync($product_id);
    wc_delete_product_transients($product_id);

    wp_safe_redirect(add_query_arg(array(
        'mvx_msg' => 'created',
        'mvx_vid' => $variation_id,
    ), get_permalink($product_id)));
    exit;
}
add_action('template_redirect', 'mvx_handle_form_submit');

/* =========================
 * UI
 * ========================= */
function mvx_render_variation_box() {
    if (is_admin()) return;
    if (!function_exists('is_product') || !is_product()) return;
    if (!mvx_user_can_manage()) return;

    $product = mvx_get_current_product();
    if (!$product) return;

    $attrs_assoc = mvx_get_all_attributes_for_form($product);
    $attrs = array_values($attrs_assoc);

    echo '<div id="mvx-box" style="position:fixed;left:20px;bottom:20px;width:560px;max-width:calc(100vw - 30px);z-index:999999;background:#fff;border:2px solid #2563eb;border-radius:14px;box-shadow:0 12px 35px rgba(0,0,0,.18);padding:14px;direction:rtl;text-align:right;">';

    echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">';
    echo '<strong style="font-size:15px;">افزودن تنوع</strong>';
    echo '<button type="button" onclick="document.getElementById(\'mvx-box\').style.display=\'none\';" style="background:#e5e7eb;border:none;border-radius:8px;padding:2px 8px;cursor:pointer;">×</button>';
    echo '</div>';

    if (isset($_GET['mvx_msg'])) {
        $msg = sanitize_text_field(wp_unslash($_GET['mvx_msg']));
        $style_ok  = 'background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;padding:8px 10px;border-radius:10px;margin-bottom:10px;';
        $style_bad = 'background:#fef2f2;color:#991b1b;border:1px solid #fecaca;padding:8px 10px;border-radius:10px;margin-bottom:10px;';
        $style_wrn = 'background:#fff7ed;color:#9a3412;border:1px solid #fdba74;padding:8px 10px;border-radius:10px;margin-bottom:10px;';

        if ($msg === 'created') {
            echo '<div style="'.$style_ok.'">تنوع ساخته شد.</div>';
        } elseif ($msg === 'exists') {
            echo '<div style="'.$style_wrn.'">این ترکیب از قبل وجود دارد.</div>';
        } elseif ($msg === 'sameattr') {
            echo '<div style="'.$style_bad.'">ویژگی ردیف اول و دوم نباید یکی باشد.</div>';
        } elseif ($msg === 'missing') {
            echo '<div style="'.$style_bad.'">ویژگی و مقدار معتبر الزامی است.</div>';
        } elseif ($msg === 'notvariable') {
            echo '<div style="'.$style_bad.'">محصول به variable تبدیل نشد.</div>';
        } elseif ($msg === 'badvalue') {
            echo '<div style="'.$style_bad.'">مقدار انتخابی معتبر نیست.</div>';
        } elseif ($msg === 'error') {
            echo '<div style="'.$style_bad.'">خطا در ساخت variation.</div>';
        }
    }

    echo '<form method="post">';
    echo wp_nonce_field('mvx_add_variation_two_rows', 'mvx_nonce', true, false);
    echo '<input type="hidden" name="mvx_action" value="add_variation_two_rows">';
    echo '<input type="hidden" name="mvx_product_id" value="' . esc_attr($product->get_id()) . '">';

    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;">';
    echo '<div>';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">ویژگی</label>';
    echo '<select id="mvx_attr1" name="mvx_attr1" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '<option value="">انتخاب ویژگی</option>';
    foreach ($attrs as $a) {
        echo '<option value="' . esc_attr($a['name']) . '">' . esc_html($a['label']) . '</option>';
    }
    echo '</select>';
    echo '</div>';

    echo '<div>';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">مقدار</label>';
    echo '<select id="mvx_val1" name="mvx_val1" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '<option value="">ابتدا ویژگی را انتخاب کنید</option>';
    echo '</select>';
    echo '</div>';
    echo '</div>';

    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;">';
    echo '<div>';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">ویژگی</label>';
    echo '<select id="mvx_attr2" name="mvx_attr2" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '<option value="">انتخاب ویژگی</option>';
    foreach ($attrs as $a) {
        echo '<option value="' . esc_attr($a['name']) . '">' . esc_html($a['label']) . '</option>';
    }
    echo '</select>';
    echo '</div>';

    echo '<div>';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">مقدار</label>';
    echo '<select id="mvx_val2" name="mvx_val2" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '<option value="">ابتدا ویژگی را انتخاب کنید</option>';
    echo '</select>';
    echo '</div>';
    echo '</div>';

    echo '<div style="margin-bottom:12px;">';
    echo '<label style="display:block;margin-bottom:5px;font-weight:700;">قیمت</label>';
    echo '<input type="number" step="any" name="mvx_price" placeholder="مثلاً 350000" style="width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;">';
    echo '</div>';

    echo '<button type="submit" onclick="return confirm(\'تنوع اضافه شود؟\');" style="width:100%;background:#2563eb;color:#fff;border:none;border-radius:10px;padding:12px;cursor:pointer;font-weight:700;">افزودن تنوع</button>';
    echo '</form>';

    $map = array();
    foreach ($attrs as $a) {
        $map[$a['name']] = $a['options'];
    }

    echo '<script>';
    echo 'window.mvxAttrMap = ' . wp_json_encode($map) . ';';
    ?>
    (function(){
        function bind(attrId, valId, otherAttrId){
            var attr = document.getElementById(attrId);
            var val  = document.getElementById(valId);
            var other = document.getElementById(otherAttrId);
            if(!attr || !val) return;

            function refill(){
                val.innerHTML = '';
                var selected = attr.value;

                if(other && other.value && selected && other.value === selected){
                    alert('این ویژگی در ردیف دیگر انتخاب شده است.');
                    attr.value = '';
                    selected = '';
                }

                if(!selected){
                    var op = document.createElement('option');
                    op.value = '';
                    op.textContent = 'ابتدا ویژگی را انتخاب کنید';
                    val.appendChild(op);
                    return;
                }

                var items = (window.mvxAttrMap && window.mvxAttrMap[selected]) ? window.mvxAttrMap[selected] : [];

                if(!items.length){
                    var op2 = document.createElement('option');
                    op2.value = '';
                    op2.textContent = 'مقداری یافت نشد';
                    val.appendChild(op2);
                    return;
                }

                var first = document.createElement('option');
                first.value = '';
                first.textContent = 'انتخاب مقدار';
                val.appendChild(first);

                items.forEach(function(item){
                    var op3 = document.createElement('option');
                    op3.value = item.value;
                    op3.textContent = item.label;
                    val.appendChild(op3);
                });
            }

            attr.addEventListener('change', refill);
            refill();
        }

        bind('mvx_attr1', 'mvx_val1', 'mvx_attr2');
        bind('mvx_attr2', 'mvx_val2', 'mvx_attr1');
    })();
    <?php
    echo '</script>';

    echo '</div>';
}
add_action('wp_footer', 'mvx_render_variation_box', 99);