ارسال کد جدید
لیست کدهای ذخیرهشده
فایرفاکس
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);
