كيف تنشئ لوحة تحكم ذكية لمراقبة المخزون في Shopify
هل سبق وقمت بحملة إعلانية ناجحة، لتكتشف في منتصف اليوم أن المنتج الأكثر مبيعاً قد نفد من المستودع؟ أو ربما لديك مئات المنتجات وتجد صعوبة في تتبع ما شارف منها على الانتهاء؟بصفتي مهتماً بتطوير تجربة التجارة الإلكترونية، سأشارككم اليوم "خلطة سحرية" تقنية لإنشاء لوحة تحكم احترافية (Inventory Dashboard) تظهر لك حالة مخزونك في ثوانٍ، مع تأثيرات بصرية رائعة وشاشة تحميل ذكية بشعار متجرك.
لماذا تحتاج هذه الصفحة في متجرك؟
الاستجابة السريعة: رؤية المنتجات المنخفضة (أقل من 10 قطع) في مكان واحد.
تنظيم المتغيرات: عرض كميات الألوان والمقاسات لكل منتج بضغطة زر.
الاحترافية: واجهة مستخدم سلسة تظهر لك كمدير متجر بشكل عصري بعيداً عن تعقيدات لوحة تحكم شوبيفاي التقليدية.
الخطوة الأولى: إنشاء التصنيف الذكي
قبل الكود، نحتاج لتجميع المنتجات التي تعاني من نقص في مكان واحد تلقائياً.اذهب إلى Products ثم Collections.
أنشئ تصنيفاً جديداً (Automated Collection).
الشرط: اختر Inventory stock ثم is less than واكتب القيمة 11.
رابط التصنيف (URL Handle): يجب أن يكون low-stock-alert (هذا الجزء حيوي لكي يعمل الكود).
الخطوة الثانية: تجهيز القالب
سندخل الآن إلى "محرر الأكواد" لإنشاء المكان المخصص لهذه الصفحة.1. إنشاء قالب الصفحة (Template)
من محرر القالب، ابحث عن مجلد Templates.أضف ملفاً جديداً باسم: page.inventory-report.json.
/*
* ------------------------------------------------------------
* Developer: Digitaneo.
*
* ------------------------------------------------------------
*/
{
"sections": {
"main": {
"type": "inventory-dashboard-admin",
"settings": {
"title": "تقرير العجز في المخزون",
"selected_collection": "low-stock-alert",
"threshold": 10
}
}
},
"order": [
"main"
]
}2. إنشاء القسم البرمجي (Section)
هذا هو "قلب" اللوحة حيث تكمن لغات البرمجة (Liquid, CSS, JavaScript).اذهب إلى مجلد Sections.
أضف ملفاً جديداً باسم: inventory-dashboard-admin.liquid.
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/[email protected]/dist/cdn.min.js" defer></script>
<div class="inventory-dashboard" x-data="inventoryManager()" x-init="init()">
<div class="loader-wrapper" x-show="!isLoaded" x-transition:leave="fade-out-transition">
<div class="loader-content">
<div class="store-logo-loader">
<img src="{{ shop.brand.logo | img_url: '300x' }}" alt="{{ shop.name }}" onerror="this.src='https://cdn.shopify.com/s/files/1/0000/0000/files/icon-store.png?v=123'">
</div>
<div class="pulse-loader"></div>
<p class="loader-text">جاري فحص المخزون...</p>
</div>
</div>
<div class="dashboard-content" x-show="isLoaded" x-transition:enter="fade-in-up">
<div class="header-section">
<h1>{{ section.settings.title }}</h1>
<div class="stats-bar">
<div class="stat-item">المجموع: <b x-text="stats.totalTroubled">0</b></div>
<div class="stat-item danger">نفذت تماماً: <b x-text="stats.outOfStock">0</b></div>
<div class="stat-item warning">تحت الحد (≤ <span x-text="thresholdLimit"></span>): <b x-text="stats.lowStock">0</b></div>
</div>
</div>
<div class="controls-area">
<input type="text" x-model="searchQuery" placeholder="🔍 ابحث في المنتجات..." class="search-bar">
<button @click="toggleSort()" class="sort-btn">
فرز المخزون: <span x-text="sortOrder === 'asc' ? '⬆️ الأقل' : '⬇️ الأكثر'"></span>
</button>
</div>
<div class="table-container">
<table class="main-table">
<thead>
<tr>
<th>المنتج</th>
<th class="text-center">المخزون</th>
<th class="text-center">الحالة</th>
<th class="text-center">الأنواع</th>
</tr>
</thead>
<template x-for="product in sortedProducts()" :key="product.id">
<tbody class="product-group">
<tr class="main-row">
<td class="product-cell">
<div class="product-flex-container">
<img :src="product.image" class="p-img" x-show="product.image" loading="lazy">
<span x-text="product.title" class="p-title-full"></span>
</div>
</td>
<td class="qty-cell">
<strong :style="getQtyStyle(product.minQty)" x-text="product.totalQty"></strong>
</td>
<td class="status-cell">
<div class="status-wrapper">
<span :class="getStatusClass(product)" class="status-indicator">
<span class="dot"></span>
<span class="status-text" x-text="getStatusText(product)"></span>
</span>
</div>
</td>
<td class="action-cell">
<button @click="product.expanded = !product.expanded" class="btn-sm">
<span class="desktop-btn" x-text="product.expanded ? 'إغلاق' : '⚙️التفاصيل'"></span>
<span class="mobile-btn" x-text="product.expanded ? '✕' : '⚙️'"></span>
</button>
</td>
</tr>
<tr x-show="product.expanded" x-transition>
<td colspan="4" class="variant-area">
<div class="v-grid">
<template x-for="v in product.variants">
<div class="v-card-new" :class="getVariantBgClass(v.qty)">
<span class="v-name" x-text="v.title"></span>
<span class="v-count" x-text="v.qty"></span>
</div>
</template>
</div>
</td>
</tr>
</tbody>
</template>
</table>
<template x-if="sortedProducts().length === 0">
<div style="text-align: center; padding: 40px; color: #2e7d32; font-family: 'Cairo';">
✅ لا يوجد منتجات تحت الحد الأدنى حالياً في هذا التصنيف.
</div>
</template>
</div>
</div>
</div>
<style>
.inventory-dashboard { direction: rtl; padding: 15px; font-family: 'Cairo', sans-serif; background: #fdfdfd; min-height: 100vh; position: relative; }
.loader-wrapper {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: #ffffff;
display: flex; align-items: center; justify-content: center;
z-index: 9999;
}
.loader-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
animation: slideFromRight 0.8s ease-out;
}
.store-logo-loader img {
width: 130px;
height: auto;
display: block;
margin: 0 auto;
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.05));
animation: pulse-img 2s infinite ease-in-out;
}
.pulse-loader {
width: 25px; height: 25px; border-radius: 50%; background-color: #222;
margin: 0 auto;
animation: pulse-dot 1.2s infinite ease-in-out;
}
.loader-text { font-family: 'Cairo'; font-weight: 700; color: #555; margin: 0; font-size: 14px; }
@keyframes slideFromRight { 0% { opacity: 0; transform: translateX(40px); } 100% { opacity: 1; transform: translateX(0); } }
@keyframes pulse-img { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.04); } }
@keyframes pulse-dot { 0% { transform: scale(0.5); opacity: 1; } 100% { transform: scale(1.8); opacity: 0; } }
.fade-out-transition { transition: opacity 0.4s ease-out; opacity: 0; pointer-events: none; }
.fade-in-up { animation: fadeInUp 0.6s ease-out forwards; }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
.header-section h1 { font-size: 1.2rem; font-weight: 700; margin-bottom: 15px; text-align: center; }
.stats-bar { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 15px; }
.stat-item { background: #fff; border: 1px solid #eee; padding: 8px 5px; border-radius: 8px; font-size: 10px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
.stat-item b { display: block; font-size: 14px; }
.stat-item.danger { color: #d32f2f; border-bottom: 3px solid #d32f2f; }
.stat-item.warning { color: #ef6c00; border-bottom: 3px solid #ff9800; }
.controls-area { display: flex; gap: 8px; margin-bottom: 15px; }
.search-bar { flex: 2; padding: 10px; border: 1px solid #ddd; border-radius: 8px; font-family: 'Cairo'; font-size: 13px; }
.sort-btn { padding: 10px; background: #fff; border: 1px solid #ccc; border-radius: 8px; font-family: 'Cairo'; font-size: 11px; cursor: pointer; }
.main-table { width: 100%; border-collapse: collapse; background: white; border-radius: 10px; overflow: hidden; table-layout: fixed; }
.main-table th { background: #f8f8f8; padding: 10px; border-bottom: 2px solid #eee; font-size: 11px; color: #777; }
.main-table td { padding: 12px 5px; border-bottom: 1px solid #f9f9f9; vertical-align: middle; }
.product-cell { width: 45%; }
.product-flex-container { display: flex; align-items: center; gap: 8px; }
.p-img { width: 35px; height: 35px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
.p-title-full { font-size: 12px; font-weight: 700; line-height: 1.3; word-break: break-word; white-space: normal; }
.qty-cell { text-align: center; width: 15%; }
.status-cell { text-align: center; width: 20%; }
.action-cell { text-align: center; width: 20%; }
.status-indicator { display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px; border-radius: 20px; font-size: 11px; color: white; font-weight: 700; }
.dot { width: 8px; height: 8px; background: white; border-radius: 50%; display: inline-block; }
.tag-red { background: #d32f2f; }
.tag-orange { background: #ef6c00; }
.tag-green { background: #2e7d32; }
.v-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; padding: 10px; background: #fcfcfc; }
.v-card-new { padding: 10px; border-radius: 8px; text-align: center; color: white; font-weight: 700; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.v-name { font-size: 10px; display: block; opacity: 0.9; }
.v-count { font-size: 16px; }
.bg-red { background-color: #d32f2f; }
.bg-orange { background-color: #ef6c00; }
.bg-green { background-color: #2e7d32; }
.btn-sm { cursor: pointer; border: 1px solid #eee; background: #fdfdfd; padding: 5px 8px; border-radius: 6px; font-family: 'Cairo'; width: 100%; }
.mobile-btn { display: inline-block; font-size: 14px; }
.desktop-btn { display: none; }
@media (max-width: 767px) {
.status-text { display: none; }
.status-indicator { padding: 6px; border-radius: 50%; }
.status-indicator .dot { margin: 0; width: 10px; height: 10px; }
.main-table th:nth-child(1), .main-table td:nth-child(1) { width: 50%; }
}
@media (min-width: 768px) {
.header-section h1 { font-size: 1.8rem; }
.stats-bar { grid-template-columns: repeat(3, 200px); justify-content: center; gap: 20px; }
.p-img { width: 45px; height: 45px; }
.p-title-full { font-size: 14px; }
.mobile-btn { display: none; }
.desktop-btn { display: inline-block; font-size: 12px; }
.v-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
}
</style>
<script>
function inventoryManager() {
return {
allProducts: [],
searchQuery: '',
sortOrder: 'asc',
stats: { outOfStock: 0, lowStock: 0, totalTroubled: 0 },
isLoaded: false,
thresholdLimit: parseInt("{{ section.settings.threshold | default: 10 }}"),
init() {
// منطق ذكي: جلب التصنيف المختار، أو جلب كل المنتجات إذا كان الخيار فارغاً
{% if section.settings.selected_collection != blank %}
{% assign target_collection = collections[section.settings.selected_collection] %}
{% else %}
{% assign target_collection = collections['all'] %}
{% endif %}
setTimeout(() => {
this.allProducts = [
{% paginate target_collection.products by 1000 %}
{% for product in target_collection.products %}
{
id: {{ product.id | json }},
title: {{ product.title | escape | json }},
image: {{ product.featured_image | img_url: '100x100' | json }},
expanded: false,
totalQty: {{ product.variants | map: 'inventory_quantity' | sum }},
variants: [
{% for variant in product.variants %}
{
title: {{ variant.title | escape | json }},
qty: {{ variant.inventory_quantity | default: 0 }}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
],
get minQty() {
// نحدد الحالة بناءً على أقل قطعة متوفرة في المنتج (Variant)
let qtys = this.variants.map(v => v.qty);
return Math.min(...qtys);
}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
{% endpaginate %}
];
this.updateStats();
this.isLoaded = true;
}, 1000);
},
updateStats() {
// الفلترة هنا تحسب فقط المنتجات التي بها نقص لضمان دقة الأرقام العلوية
const troubled = this.allProducts.filter(p => p.minQty <= this.thresholdLimit);
this.stats.outOfStock = troubled.filter(p => p.minQty <= 0).length;
this.stats.lowStock = troubled.filter(p => p.minQty > 0 && p.minQty <= this.thresholdLimit).length;
this.stats.totalTroubled = troubled.length;
},
getStatusText(product) {
if (product.minQty <= 0) return 'نفذ';
if (product.minQty <= this.thresholdLimit) return 'منخفض';
return 'سليم';
},
getStatusClass(product) {
if (product.minQty <= 0) return 'tag-red';
if (product.minQty <= this.thresholdLimit) return 'tag-orange';
return 'tag-green';
},
getVariantBgClass(qty) {
if (qty <= 0) return 'bg-red';
if (qty <= this.thresholdLimit) return 'bg-orange';
return 'bg-green';
},
getQtyStyle(minQty) {
if (minQty <= 0) return 'color: #d32f2f;';
if (minQty <= this.thresholdLimit) return 'color: #ef6c00;';
return 'color: #2e7d32;';
},
toggleSort() {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
},
sortedProducts() {
// الفلترة النهائية للجدول: استبعاد المنتجات السليمة تماماً
return this.allProducts
.filter(p => {
const isTroubled = p.minQty <= this.thresholdLimit;
const matchesSearch = p.title.toLowerCase().includes(this.searchQuery.toLowerCase());
return isTroubled && matchesSearch;
})
.sort((a, b) => {
return this.sortOrder === 'asc' ? a.totalQty - b.totalQty : b.totalQty - a.totalQty;
});
}
}
}
</script>
{% schema %}
{
"name": "لوحة تتبع المخزون",
"settings": [
{
"type": "text",
"id": "title",
"label": "العنوان الرئيسي",
"default": "⚠️ تقرير حالة المخزون"
},
{
"type": "collection",
"id": "selected_collection",
"label": "اختر التصنيف",
"info": "اتركه فارغاً لفحص كافة المنتجات"
},
{
"type": "range",
"id": "threshold",
"min": 2,
"max": 50,
"step": 1,
"unit": "Pcs",
"label": "حد التنبيه",
"default": 10
}
]
}
{% endschema %}
الخطوة الثالثة: إطلاق اللوحة (The Launch)
بعد تجهيز الأكواد، حان وقت إظهارها للنور:إنشاء الصفحة: اذهب إلى Online Store > Pages وأنشئ صفحة جديدة باسم "تقرير المخزون".
اختيار القالب: من القائمة الجانبية (Theme Template)، اختر inventory-report.
التخصيص البصري: اذهب إلى Customize (محرر القالب المرئي)، اختر الصفحة التي أنشأتها، وقم بإضافة قسم Inventory Dashboard Admin.
تخصيص الحد الأدني: يمكنك تخصيص الحد الأندى للتنبيه من 2 منتجات الى 20 منتج
تخصيص التصنيف: اختر التصنيف الذي ترغب في عرض مخزونه فقط أو اتركه فارغا لحساب مخزون جميع المنتجات
تتبع المواقع: إذا كان لديك أكثر من مستودع (Locations)، تأكد أن الكود يسحب "إجمالي المخزون" لضمان دقة البيانات.
تحديث البيانات: الصفحة تعتمد على التحديث التلقائي للتصنيف الذكي أو اي تصنيف تختاره كما يمكن جلب البيانات من جميع المنتجات، لذا قد يستغرق ظهور المنتج الجديد في اللوحة بضع ثوانٍ بعد وصوله لشرط "أقل من 11".
الحماية: أنصحك بجعل هذه الصفحة "مخفية" عن محركات البحث (No-index) أو عدم وضع رابطها في قائمة التذييل العامة لتبقى خاصة بمديري المتجر فقط.
هل أعجبتكم هذه الطريقة؟ شاركوني في التعليقات إذا كنتم تريدون شروحات برمجية أكثر لتطوير متاجركم!
تخصيص التصنيف: اختر التصنيف الذي ترغب في عرض مخزونه فقط أو اتركه فارغا لحساب مخزون جميع المنتجات
💡 إرشادات إضافية لنجاح التجربة
بصفتي جربت هذه الطريقة، إليكم هذه النصائح الذهبية:تتبع المواقع: إذا كان لديك أكثر من مستودع (Locations)، تأكد أن الكود يسحب "إجمالي المخزون" لضمان دقة البيانات.
تحديث البيانات: الصفحة تعتمد على التحديث التلقائي للتصنيف الذكي أو اي تصنيف تختاره كما يمكن جلب البيانات من جميع المنتجات، لذا قد يستغرق ظهور المنتج الجديد في اللوحة بضع ثوانٍ بعد وصوله لشرط "أقل من 11".
الحماية: أنصحك بجعل هذه الصفحة "مخفية" عن محركات البحث (No-index) أو عدم وضع رابطها في قائمة التذييل العامة لتبقى خاصة بمديري المتجر فقط.
خاتمة
الفرق بين التاجر الناجح والتاجر العادي هو "البيانات". عندما تمتلك لوحة تحكم تخبرك بالخطر قبل وقوعه، فأنت تحمي استثمارك وتضمن استمرار مبيعاتك.هل أعجبتكم هذه الطريقة؟ شاركوني في التعليقات إذا كنتم تريدون شروحات برمجية أكثر لتطوير متاجركم!
