بازگشت به مقاله‌ها
ERPتاریخ انتشار: ۱۹ اسفند ۱۴۰۴زمان مطالعه: حدود ۱۰ دقیقه

وقتی edit کردن کافی نبود: Editionهای مهندسی در یک ERP تولیدی

روایت frontend یک فرایند مهندسی که باید تعریف قبلی را حفظ می‌کرد، revision جدید می‌ساخت و تفاوت Partهای جاری و تاریخی را جدی می‌گرفت.

مهندسی محصولگردش کارReact

صفحه‌های اولیه Tag و Part از بیرون خیلی عجیب نبودند. یک list، یک form، چند lookup و چند action معمولی.

مشکل وقتی شروع شد که Engineering بعد از جلو رفتن یک تعریف در workflow دوباره نیاز به تغییر داشت.

تا قبل از آن، مدل ذهنی خیلی ساده بود:

record → edit → save

ولی اینجا دیگر همیشه نمی‌شد همان row قبلی را edit کرد.

اگر یک Part از تعریف جدید حذف می‌شد، نباید از نسخه‌ای که قبلاً براساس آن کار شده بود هم ناپدید می‌شد. اگر Part یا quantity تغییر می‌کرد، نسخه قبلی باید می‌ماند و معلوم می‌شد تغییر جدید متعلق به کدام revision است.

از اینجا به بعد مسئله دیگر form و CRUD نبود. باید history خودش بخشی از مدل می‌شد.

Project تمام می‌شد، ولی کار Engineering تازه شروع می‌شد

Project در Sales شکل می‌گرفت و می‌توانست چند Tag داشته باشد.

بعد از تأیید نهایی Project، بخش مرتبط وارد Engineering می‌شد. این handoff را نمی‌شد فقط با اضافه کردن چند field مهندسی به همان Project form حل کرد.

Project workflow و Tag Edition workflow دو چیز جدا بودند.

اولی مشخص می‌کرد کار چه زمانی از Sales وارد مرحله بعد شده، ولی دومی lifecycle خود تعریف مهندسی را کنترل می‌کرد.

frontend باید Project را به‌عنوان context نگه می‌داشت، ولی Engineering فضای خودش را لازم داشت.

به‌شکل خیلی ساده:

Sales Project
      ↓
Final Approval
      ↓
Engineering
      ↓
Edition 0
      ↓
Completed Definition
      ↓
Revision Needed?
      ↓
Edition 1 / 2 / ...

Edition 0 فقط یک عدد نبود

Edition صفر اولین تعریف مهندسی بعد از handoff بود.

اطلاعات Tag، محصول‌ها، Groupها و Part List به‌مرور داخل همین context کامل می‌شدند.

یک دوره frontend شماره Edition را به‌صورت input اجباری نمایش می‌داد، در حالی که backend خودش براساس آخرین Edition شماره بعدی را تعیین می‌کرد.

این از آن inconsistencyهایی بود که نشان می‌داد مفهوم هنوز کاملاً در UI جا نیفتاده.

وقتی Edition ترتیب revisionها را نشان می‌دهد، منطقی نیست کاربر خودش هر عددی وارد کند.

Edition بعدی هم قرار نبود مستقل و بدون ارتباط ساخته شود. باید ادامه یک تعریف قبلی باشد و براساس state همان جریان جلو برود.

تغییر Part یعنی ساخت history، نه overwrite کردن آن

در Edition جدید، یک Part می‌توانست نسبت به نسخه قبل:

  • Added باشد
  • Removed باشد
  • Modified باشد

این سه state فقط واژه‌های مقاله نیستند؛ همان سه وضعیت معنی‌دار و قابل نمایش در مدل revision بودند. وضعیت داخلی Unknown هم در بخشی از implementation وجود داشت، اما برای روایت تغییر واقعی بین دو Edition، Added، Removed و Modified همان چیزی بودند که کاربر باید می‌دید.

مثلاً:

Edition 0
├── Part A
├── Part B
└── Part C

        ↓

Edition 1
├── Part A
├── Part B'   Modified
├── Part D    Added
└── Part C    Removed

برای Modified فقط داشتن مقدار جدید کافی نبود. row جدید باید می‌دانست از کدام Part قبلی آمده.

هم برای نمایش تغییر لازم بود، هم برای اینکه مراحل بعدی یک Part جدید را با نسخه اصلاح‌شده یک Part قبلی اشتباه نگیرند.

رفتار update هم بسته به این فرق می‌کرد که row از کجا آمده.

اگر Part از قبل متعلق به draft جاری بود، همان row می‌توانست edit شود.

ولی اگر از Edition قبلی به ارث رسیده بود، تغییر باید داخل Edition جدید ثبت می‌شد و ارتباطش با نسخه قبل حفظ می‌شد.

delete هم برای یک Part تاریخی به معنی پاک کردن گذشته نبود. حذف باید خودش به‌عنوان تغییر در revision جدید ثبت می‌شد.

نسخه‌های اولیه این flow کامل نبودند. بعضی lookupها موقت بودند و انتخاب Part قبلی هنوز به شکلی انجام می‌شد که برای lineage نهایی مناسب نبود.

بعداً این ارتباط به خود عملیات revision منتقل شد و دیگر قرار نبود کاربر با یک selector حدس بزند Part جدید ادامه کدام row قبلی است.

این ساختار را نمی‌شد تا ابد داخل modal نگه داشت

Project، Tag، Edition، Group و Part پنج سطح context روی هم داشتند؛ و موقعیت nested هر Part می‌توانست hierarchy را از این هم عمیق‌تر کند.

تا وقتی child فقط یک فرم کوچک است، modal جواب می‌دهد.

ولی وقتی همان child خودش table، form، validation، workflow و حتی child دیگری دارد، modal داخل modal خیلی زود تبدیل می‌شود به چیزی که نه navigation خوبی دارد و نه state آن قابل اعتماد می‌ماند.

برای این بخش resourceها را به routeهای مستقل بردم.

کاربر وقتی وارد Partها می‌شد هنوز context Tag و Group را داشت. refresh هم context را از بین نمی‌برد و لینک مستقیم به همان صفحه قابل استفاده بود.

جزئیات routeهای داخلی اهمیتی ندارند. چیزی که مهم بود این بود:

Project
  ↓
Tag
  ↓
Edition
  ↓
Group
  ↓
Part

parent identity بخشی از URL و navigation شد، نه state موقتی که فقط تا باز بودن یک modal زنده بماند.

create کردن resource nested هم باید دقیق بود.

parent از route می‌آمد، داخل field درست form قرار می‌گرفت و در آن context قابل تغییر نبود.

اوایل بعضی اسم routeها و fieldها واقعاً چیزی را که حمل می‌کردند خوب نشان نمی‌دادند. همین باعث شد mappingها explicitتر شوند و کمتر روی conventionهای حدسی حساب کنیم.

history باید می‌ماند، ولی نباید وارد هر validationی می‌شد

بعد از اضافه شدن Edition، یکی از دردسرهای اصلی این بود که فرق بین history و current state در همه مسیرها رعایت شود.

rowهای Editionهای قبلی باید باقی می‌ماندند.

اما باقی ماندنشان به این معنی نبود که validation نسخه جاری هم باید آنها را حساب کند.

یک completeness check قدیمی Partهای چند Edition را با هم traverse می‌کرد.

در چنین حالتی ممکن بود Edition جاری ناقص باشد، ولی داده تاریخی باعث شود کامل دیده شود.

یا validation درباره rowهایی تصمیم بگیرد که اصلاً دیگر تعریف فعال نیستند.

مدل درست این بود که completeness روی Edition مرتبطِ فعلی بررسی شود.

Editionهای قبل برای history و مقایسه‌اند.

این مرز در همه‌جای implementation یک‌دست نبود. در بعضی stageهای جدیدتر filtering براساس Edition درست شده بود، ولی مسیرهای قدیمی هنوز history را با state فعلی مخلوط می‌کردند.

report هم دقیقاً همین مشکل را داشت.

«همه Partهایی که زمانی وجود داشته‌اند» با «Part List معتبر فعلی» یکی نیست.

quantity قرار نبود مجوز rebuild داده‌ها باشد

یک مسئله دیگر از quantityهای مورد انتظار آمد.

Project تعداد Tag مورد انتظار داشت و هر Tag هم تعداد محصول مورد انتظار خودش را.

راه ساده این بود که اگر quantity تغییر کرد، childها پاک شوند و دوباره ساخته شوند.

در اولین نگاه حتی منطقی به نظر می‌رسد.

ولی Tag یا Product ممکن است تا آن زمان کلی اطلاعات، relation، فایل یا داده مهندسی داشته باشد.

تغییر یک عدد نباید بتواند همه این‌ها را بی‌سروصدا نابود کند.

پس quantity را به‌عنوان constraint در نظر گرفتیم، نه فرمان sync.

Expected Count
      ↓
Compare With Actual
      ↓
Mismatch
      ↓
Block Completion
      ↓
User Fixes It Explicitly

اگر mismatch وجود داشت، سیستم completion را متوقف می‌کرد تا کاربر خودش تصمیم بگیرد رکورد اضافه کند، حذف کند یا expected count را تغییر دهد.

چند branch قدیمی هنوز رفتار clear-and-recreate داشتند. بعد از روشن شدن business rule، دیگر نمی‌شد آن‌ها را implementation نهایی در نظر گرفت.

حتی Good هم همیشه identity کافی نبود

داخل Part List ممکن بود یک item یکسان در چند جای assembly استفاده شود.

مثلاً:

3
└── 3.2
    └── 3.2.1

پس اینکه دو row Good یکسان داشته باشند، الزاماً به معنی این نبود که همان Part هستند.

موقعیت hierarchical هم بخشی از context بود.

این rule در نسخه‌های اولیه در همه مسیرها یکسان پیاده نشده بود.

مثلاً pre-check مربوط به copy، item و nested position را با هم می‌دید، ولی خود copy فقط item را مقایسه می‌کرد و حتی position را هم منتقل نمی‌کرد.

نتیجه‌اش مشخص بود: دو Part با item یکسان ولی جای متفاوت ممکن بود duplicate در نظر گرفته شوند.

اگر امروز این قسمت را دوباره می‌ساختم، equivalence را یک rule مشترک می‌کردم که copy، revision، duplicate check و validation همگی از همان استفاده کنند.

چنین چیزی نباید در چهار service مختلف چهار تعریف نزدیک ولی متفاوت داشته باشد.

workflow فقط نباید در backend وجود داشته باشد

داشتن API برای edit به این معنی نیست که edit در هر stateی مجاز است.

ساخت Group، تغییر Part یا ایجاد revision جدید باید با Edition جاری و workflow آن هماهنگ می‌بود.

این gateها در طول توسعه چند بار اصلاح شدند.

یک نسخه حتی ساخت Group را منوط به کامل شدن Edition کرده بود، در حالی که خود Group برای کامل کردن همان Edition لازم بود. یعنی rule از نظر فنی وجود داشت، ولی از نظر flow عملاً مسیر را بسته بود.

frontend باید بین این حالت‌ها فرق می‌گذاشت:

  • دیدن history
  • ویرایش draft جاری
  • شروع revision جدید

فقط disabled کردن یک form برای بیان این تفاوت کافی نبود.

route، context، change status و actionهای قابل دسترس باید همه یک state واحد را نشان می‌دادند.

اگر امروز از اول می‌ساختم

قبل از ساختن formها سه چیز را خیلی زودتر مشخص می‌کردم:

  1. Edition فعال دقیقاً کدام است؟
  2. دو Part در دو revision چه زمانی معادل‌اند؟
  3. در هر state چه operationهایی مجازند؟

بعد query، validation، copy، report و action policy باید همگی از همین قراردادها استفاده کنند.

routeهای nested را همچنان نگه می‌داشتم. این resourceها آن‌قدر state و history دارند که فضای مستقل برایشان ارزش دارد.

lineage را هم explicit نگه می‌داشتم. اینکه frontend دو snapshot را مقایسه کند و بعد حدس بزند چه چیزی Modified شده، برای چنین داده‌ای زیادی شکننده است.

Edition به‌خودی‌خود سخت‌ترین بخش نبود.

سختی واقعی این بود که تمام مسیرهای سیستم روی یک تعریف از «Edition جاری» توافق داشته باشند.

اگر validation تاریخچه را با نسخه فعلی قاطی کند، copy موقعیت Part را فراموش کند یا report Edition اشتباهی را بخواند، UI ممکن است کاملاً درست به نظر برسد ولی نتیجه همچنان غلط باشد.

ERP۱۲ تیر ۱۴۰۵حدود ۱۰ دقیقه

از CRUDهای تکراری تا PageShell در یک ERP واقعی

روایت شکل‌گیری PageShell؛ از wiring تکراری pageهای CRUD تا یک قرارداد تایپ‌شده که orchestration مشترک را از behavior دامنه جدا نگه می‌دارد.

معماری فرانت‌اندTypeScript
ERP۳ آبان ۱۴۰۵حدود ۹ دقیقه

table مشترک در یک ERP: state مال کیست؟

اینکه یک table ساده‌ی client-side چطور به دو table جدا برای client و server رسید، چرا TanStack Table را امتحان کردم و نگه نداشتم، و state جدول‌ها چطور از URL به storeهای جدا برای هر table رسید.

معماری فرانت‌اندمدیریت وضعیت