شرح Node.js وCloud Firestore: بناء نظام جرد منزلي عملي خطوة بخطوة

دقائق القراءة: 8

مقدمة: لماذا تحتاج إلى نظام جرد منزلي ذكي؟

إدارة المشتريات المنزلية بشكل يدوي قد تبدو مهمة بسيطة في البداية، لكنها تتحول سريعاً إلى عبء يومي مع تكرار شراء المواد الغذائية، والتوابل، والأدوية، والمنتجات الاستهلاكية الأخرى. كثيراً ما نكتشف منتجات منسية داخل الخزائن أو نعيد شراء عناصر موجودة بالفعل بسبب غياب نظام واضح للتتبع.

هنا تظهر فائدة بناء نظام جرد منزلي باستخدام Node.js وCloud Firestore. هذا النوع من الأنظمة يمنحك وسيلة منظمة لتسجيل المنتجات، ومتابعة الكميات، وفرز العناصر، والبحث عنها، وتحديث بياناتها أو حذفها عند الحاجة.

واجهة توضيحية لمشروع نظام جرد منزلي باستخدام Node.js وCloud Firestore

في هذا المقال سنعيد بناء الفكرة بصورة عملية ومبسطة، مع شرح تقني واضح يساعدك على تطوير تطبيق فعلي قابل للتوسعة لاحقاً.

ما الذي سيوفره نظام الجرد المنزلي؟

عند تصميم هذا المشروع، الهدف ليس مجرد تخزين بيانات المنتجات، بل إنشاء نظام يساعد المستخدم على إدارة المخزون المنزلي بكفاءة. من أهم الوظائف التي ينبغي أن يدعمها التطبيق:

  • إضافة سجل لكل منتج مع السعر والكمية وتاريخ الانتهاء.
  • تصفية العناصر بحسب السعر أو الكمية أو تاريخ الصلاحية.
  • فرز النتائج وفق معايير محددة.
  • تحديث بيانات المنتجات الموجودة مسبقاً.
  • حذف العناصر التي لم تعد مستخدمة أو نفدت.
  • تسهيل عرض البيانات داخل واجهة بسيطة وسريعة.

فهم بنية قاعدة البيانات في Cloud Firestore

تُعد Cloud Firestore قاعدة بيانات NoSQL سحابية مرنة وقابلة للتوسع، وهي جزء من منظومة Firebase. تعتمد على تخزين البيانات داخل Documents، ويتم تنظيم هذه المستندات ضمن Collections.

إذا أردنا بناء نظام جرد منزلي، فمن المنطقي أن نستخدم Collection لكل فئة من المنتجات، مثل الوجبات الخفيفة أو البهارات أو الأدوية. داخل كل فئة، يمثّل كل Document منتجاً واحداً، بينما تحتوي حقول المستند على بيانات مثل السعر والكمية وتاريخ الانتهاء.

تصور مبسط لهيكل البيانات

"Snacks" : {
  "Food_Item_1" : {
    "Price" : P1,
    "Quantity" : Q1,
    "ExpiryDate" : D1
  },
  "Food_Item_2" : {
    "Price" : P2,
    "Quantity" : Q2,
    "ExpiryDate" : D2
  },
  .
  .
  "Food_Item_N" : {
    "Price" : PN,
    "Quantity" : QN,
    "ExpiryDate" : DN
  }
}

في هذا المثال:

  • Snacks هو اسم المجموعة.
  • Food_Item_1 وFood_Item_2 أسماء مستندات تمثل المنتجات.
  • Price وQuantity وExpiryDate هي الحقول المخزنة داخل كل مستند.

هذه البنية تجعل الوصول إلى البيانات واسترجاعها أكثر وضوحاً، خاصة عند تنفيذ عمليات البحث والترتيب والتحديث.

إنشاء تطبيق Express لاستقبال بيانات المستخدم

لبناء الواجهة الخلفية، سنعتمد على Express داخل بيئة Node.js. الخطوة الأولى هي تعريف المسارات الأساسية وتجهيز التطبيق لاستقبال البيانات من النماذج.

const express = require("express")
const app = express()

//Middleware to parse data in body portion of incoming request, like POST
//request
const body_parser = require("body-parser")
objForUrlencoded = body_parser.urlencoded({ extended: false })

app.set("view engine", "ejs")
app.use("/assets", express.static("assets"))
app.use(objForUrlencoded)

app.get("/", (req, res, next) => {
  //Show the homepage
  res.render("homepage")
})

app.get("/save_data.ejs", (req, res, next) => {
  //Show the form for saving data
  res.render("save_data")
})

app.get("/search_data.ejs", (req, res, next) => {
  //Show the form for searching data
  res.render("search_data")
})

app.listen(1337, () => {
  console.log("Listening on port 1337")
})

الكود السابق يوضح إعداد تطبيق بسيط يعمل على المنفذ 1337، ويستخدم المحرك ejs لعرض الصفحات، كما يعتمد على الوسيط body-parser لتحليل البيانات الواردة من طلبات POST.

أهمية حقول الإدخال داخل النماذج

عند تصميم نموذج الإدخال، يجب أن يحتوي كل عنصر input أو select على الخاصية name، لأن هذه القيمة ستكون المفتاح الذي نستخدمه لاحقاً للوصول إلى البيانات داخل req.body.

<input type="text" name="productName">
<br />
<br />
<label for="productCategory">Product Category:</label>
<select name="productCategory">
  <option value="Snacks">Snacks</option>
  <option value="Biscuits">Biscuits</option>
  <option value="Spices">Spices</option>
</select>
<br />
<br />
<label for="price">Price:</label>
<input type="number" name="price">
<br/><br/>
<label for="quantity">Quantity:</label>
<input type="number" name="quantity">

مثلاً، يمكن الوصول إلى اسم المنتج من خلال productName، وإلى الفئة من خلال productCategory، وكذلك السعر والكمية بالطريقة نفسها.

ربط Node.js مع Cloud Firestore

بعد تجهيز التطبيق لاستقبال البيانات، ننتقل إلى خطوة الربط مع قاعدة البيانات. يتم ذلك عبر Firebase Admin SDK باستخدام حساب خدمة Service Account ومفتاح سري بصيغة JSON.

واجهة بسيطة لنظام الجرد المنزلي قبل تخزين البيانات في Cloud Firestore

/*Set up Admin API for Firebase*/
const admin = require('firebase-admin');

//Define path to secret key generated for service account
const serviceAccount = require(PATH TO KEY);

//Initialize the app
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

في هذا الجزء نستخدم ملف المفتاح السري الذي يتم توليده عند إنشاء حساب الخدمة. ويفضل في البيئات الإنتاجية عدم وضع هذا المسار بشكل مباشر داخل المشروع، بل تخزينه عبر متغيرات البيئة لحماية البيانات الحساسة.

حفظ البيانات داخل Firestore

بعد التهيئة، يصبح بإمكاننا إنشاء مرجع إلى قاعدة البيانات باستخدام admin.firestore() ثم حفظ البيانات عبر الدالة set().

let db = admin.firestore()

//Depending on your schema, save data by specifying the collection name,
//document name and data contents as follows
await db.collection(key).doc(prod).set(save_to_database[key][prod])

الفكرة هنا بسيطة:

  • collection(key) تحدد اسم الفئة.
  • doc(prod) تحدد اسم المنتج أو المستند.
  • set(...) تحفظ القيم داخل المستند.

مصطلحات مهمة في Firestore

أثناء قراءة توثيق Firestore ستصادف عدة مفاهيم أساسية، ومن المهم فهمها قبل متابعة بناء المشروع:

  • CollectionReference: كائن يُستخدم لإضافة مستندات أو الوصول إليها أو تنفيذ استعلامات عليها.
  • DocumentReference: مرجع إلى موقع مستند معين داخل قاعدة البيانات للقراءة أو الكتابة أو الاستماع للتغييرات.
  • QuerySnapshot: نتيجة استعلام تحتوي على مجموعة من المستندات المطابقة.
  • DocumentSnapshot: تمثيل لمستند واحد تمّت قراءته من القاعدة، ويمكن استخراج بياناته باستخدام .data().

كيفية استرجاع البيانات والاستعلام عنها

بعد تخزين عدد كافٍ من السجلات، تصبح الاستفادة الحقيقية من Cloud Firestore مرتبطة بإمكانات البحث والاستعلام. يمكن للتطبيق مثلاً عرض جميع المنتجات ضمن فئة محددة، أو حساب إجمالي الأسعار، أو ترتيب العناصر حسب الكمية أو السعر.

واجهة البحث وتصفية المنتجات داخل نظام الجرد المنزلي باستخدام Firestore

الحصول على كل المنتجات ضمن فئة محددة

//Get all docs under the given category
helper_func_get_data = async (category, db) => {
  const data = await db.collection(category).get()
  if (data.empty) {
    return -1
  } else return data
}

يعيد هذا الاستعلام كل المستندات الموجودة داخل المجموعة المحددة. وإذا لم تكن هناك أي بيانات، يمكن التحقق من ذلك عبر الخاصية .empty.

قراءة نتائج الاستعلام باستخدام QuerySnapshot

كل عملية استعلام تعيد كائناً من النوع QuerySnapshot. ويمكنك المرور على المستندات الناتجة باستخدام forEach() كما يلي:

data.forEach((doc) => {
  Product_Info[doc.id] = doc.data()
})

//Here data is a QuerySnapshot and Product_Info is a JavaScript object
//with document names as keys and their corresponding values. We can pass this
//object as an argument in render() method to display the received contents

بهذه الطريقة يمكن تحويل النتائج إلى كائن JavaScript سهل الإرسال إلى الواجهة لعرض محتوى المنتجات للمستخدم.

حساب إجمالي قيمة المنتجات

إذا أردت حساب مجموع الأسعار أو الكميات داخل فئة معينة، يمكنك الاعتماد على التجميع اليدوي أثناء المرور على البيانات:

total_agg = 0
data.forEach((doc) => {
  total_agg += doc.data()[aggregate_over]
})

//aggregate_over is a variable which defines criteria to sum over like price
//or quantity

المتغير aggregate_over يحدد الحقل المراد جمعه، مثل Price أو Quantity.

فرز النتائج حسب السعر أو أي معيار آخر

يوفر Firestore دالة orderBy() لفرز البيانات مباشرة من قاعدة البيانات قبل إرجاعها إلى التطبيق:

const data = await db.collection(category).orderBy(filter_criteria).get()
where filter_criteria = "Price".

ويمكنك تغيير قيمة filter_criteria إلى أي حقل آخر مثل Quantity أو ExpiryDate حسب طبيعة العرض المطلوبة.

حذف العناصر غير المستخدمة من قاعدة البيانات

مع مرور الوقت، ستستهلك بعض المنتجات أو تصبح غير ضرورية، وهنا تظهر أهمية حذف السجلات القديمة للحفاظ على دقة البيانات وتناسقها. في نظام الجرد المنزلي، تمثل هذه العملية خطوة أساسية للحفاظ على مخزون واقعي وحديث.

firebase_delete_data = async (category, response, product_name) => {
  try {
    let db = admin.firestore()
    await db.collection(category).doc(product_name).delete()
    response.render("search_data")
  } catch (err) {
    console.log(err)
  }
}

تستخدم الدالة delete() لحذف مستند معين داخل الفئة المطلوبة بناءً على اسم المنتج. ومن الأفضل في التطبيقات الفعلية عرض رسالة نجاح أو فشل واضحة للمستخدم بدلاً من العودة المباشرة إلى الصفحة فقط.

تحديث بيانات المنتجات الموجودة

من الطبيعي أن تتغير بيانات المنتجات بمرور الوقت، سواء بسبب تغير السعر أو نقص الكمية أو تعديل تاريخ الانتهاء. لذلك من الضروري دعم خاصية التحديث داخل التطبيق.

firebase_update_data = async (category, response, reqbody) => {
  try {
    let db = admin.firestore()
    await db.collection(category).doc(reqbody["productName"]).update({
      "Price": parseFloat(reqbody["price"]),
      "Quantity": parseFloat(reqbody["quantity"]),
      "ExpiryDate": reqbody["expiryDate"]
    })
    response.render("successpage")
  } catch (err) {
    console.log(err)
    response.render("failurepage")
  }
}

في هذا المثال:

  • يتم تحديد المستند من خلال اسم المنتج داخل reqbody["productName"].
  • يتم تحويل القيم الرقمية باستخدام parseFloat() لضمان تخزينها كأرقام فعلية.
  • تستخدم الدالة update() لتعديل الحقول دون الحاجة إلى إعادة إنشاء المستند بالكامل.

واجهة تحديث تفاصيل المنتج داخل تطبيق الجرد المنزلي

تنظيم وظائف CRUD داخل ملفات مستقلة

كلما توسع التطبيق، يصبح من الأفضل فصل منطق قاعدة البيانات عن ملف الخادم الرئيسي. لذلك يمكن تجميع وظائف الحفظ والاسترجاع والحذف والتحديث داخل ملف مستقل ثم تصديرها.

module.exports = {
  "firebase_save_data": firebase_save_data,
  "firebase_retrieve_data": firebase_retrieve_data,
  "firebase_delete_data": firebase_delete_data,
  "firebase_update_data": firebase_update_data
}

وبعد ذلك يتم استيراد هذه الوظائف داخل تطبيق Express بالشكل التالي:

const firebase_functions = require("./firebase_CRUD_custom_code/firebase_functions.js")

هذا الأسلوب يمنح المشروع مزايا مهمة:

  • سهولة الصيانة والتطوير.
  • تقليل تداخل المسؤوليات داخل الملفات.
  • إعادة استخدام الدوال في أكثر من مسار.
  • تحسين وضوح البنية البرمجية للتطبيق.

مثال على ربط مسار التحديث بالدالة المناسبة

app.post("/update", objForUrlencoded, (req, res) => {
  firebase_functions.firebase_update_data(req.body["category"], res, req.body)
})

يستقبل هذا المسار طلب التحديث من النموذج، ثم يمرر الفئة والبيانات إلى الدالة المسؤولة عن تعديل السجل داخل Firestore.

أفضل ممارسات لتحسين المشروع تقنياً وعملياً

رغم أن المثال السابق يحقق الأساسيات، فإن هناك عدداً من التحسينات التي تجعل النظام أكثر قوة واعتمادية:

التحقق من صحة البيانات

  • تأكد من أن السعر والكمية قيم رقمية صحيحة.
  • تحقق من أن اسم المنتج غير فارغ.
  • امنع القيم السالبة في الكمية أو السعر.
  • افحص تنسيق تاريخ الانتهاء قبل حفظه.

تحسين الأمان

  • لا تضع ملف Service Account داخل مستودع عام.
  • استخدم متغيرات البيئة لتخزين المسارات والمفاتيح الحساسة.
  • أضف صلاحيات دقيقة في إعدادات Firebase متى كان ذلك ممكناً.

تحسين تجربة المستخدم

  • أظهر رسائل نجاح وفشل واضحة بعد كل عملية.
  • أضف تنبيهات للعناصر القريبة من تاريخ الانتهاء.
  • وفر ميزة البحث باسم المنتج مباشرة.
  • اعرض المنتجات منخفضة الكمية في قسم مستقل.

إمكانيات التوسع مستقبلاً

يمكنك تطوير هذا النظام مستقبلاً ليشمل مزايا إضافية مثل:

  1. تسجيل دخول المستخدمين وربط كل منزل بمخزون مستقل.
  2. إضافة صور للمنتجات.
  3. بناء لوحة تحكم بإحصاءات شهرية.
  4. إرسال تنبيهات عند قرب انتهاء الصلاحية.
  5. ربط التطبيق بواجهة API أو تطبيق جوال.

لماذا يُعد هذا المشروع مناسباً للتعلم العملي؟

هذا المشروع يجمع بين عدة مهارات مهمة في تطوير الويب الحديث:

  • التعامل مع Node.js على مستوى الخادم.
  • استخدام Express لبناء المسارات والواجهات.
  • التكامل مع قاعدة بيانات سحابية مثل Cloud Firestore.
  • فهم عمليات CRUD الأساسية.
  • تنظيم الكود البرمجي بشكل قابل للتوسع.

كما أن فكرته عملية وقابلة للتطبيق في الحياة اليومية، وهذا يمنح المتعلم حافزاً أكبر للاستمرار وتطوير المشروع بدلاً من الاكتفاء بمثال نظري تقليدي.

الخلاصة النهائية لمشروع نظام الجرد المنزلي باستخدام Node.js وCloud Firestore

الخلاصة التقنية

بناء نظام جرد منزلي باستخدام Node.js وCloud Firestore يُعد مشروعاً ممتازاً لفهم كيفية تصميم تطبيقات تعتمد على البيانات في الزمن الحقيقي أو شبه الحقيقي. قوة هذا النموذج لا تكمن فقط في عمليات الحفظ والاسترجاع، بل في مرونة بنية Firestore وسهولة تنفيذ الاستعلامات والتحديثات عليها. ومن الناحية التقنية، فإن فصل منطق قاعدة البيانات عن مسارات Express، والتحقق من صحة المدخلات، وتأمين المفاتيح الحساسة، كلها ممارسات ضرورية لتحويل المشروع من تجربة تعليمية إلى تطبيق موثوق وقابل للتوسع.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *