@devhouse.int/angular-reusable-components 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -1,3 +1,517 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @devhouse.int/angular-reusable-components
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
مكتبة مكوّنات Angular قابلة لإعادة الاستخدام لمشاريع DevHouse — مبنية على **Angular 20**
|
|
4
|
+
(standalone components) و**DevExtreme 26.1.4**، وتدعم العربية وRTL.
|
|
5
|
+
|
|
6
|
+
المكتبة **مفكوكة الارتباط عن أي تطبيق**: كل ما تحتاجه من خدمات (بيانات، صلاحيات، ترجمة،
|
|
7
|
+
إشعارات، سير عمل …) يصلها من المشروع المستضيف عبر **injection tokens**، فلا تعتمد على
|
|
8
|
+
خدمات مشروع بعينه.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## المتطلبات
|
|
13
|
+
|
|
14
|
+
```jsonc
|
|
15
|
+
"@angular/common" "^20.1.0"
|
|
16
|
+
"@angular/core" "^20.1.0"
|
|
17
|
+
"@angular/forms" "^20.1.0"
|
|
18
|
+
"@angular/router" "^20.1.0"
|
|
19
|
+
"devextreme" "^26.1.4"
|
|
20
|
+
"devextreme-angular" "^26.1.4"
|
|
21
|
+
"leaflet" "^1.9.4" // لـ MapLocationPicker فقط
|
|
22
|
+
"rxjs" "^7.8.0"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## التثبيت
|
|
26
|
+
|
|
27
|
+
للتجربة من npm:
|
|
28
|
+
>
|
|
29
|
+
> ```sh
|
|
30
|
+
> npm install @devhouse.int/angular-reusable-components@0.1.0
|
|
31
|
+
> ```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## ⚠️ اقرأ هذا أولاً — قاعدة الـProviders
|
|
36
|
+
|
|
37
|
+
بعض المكوّنات تحتاج providers للوصول إلى بيانات التطبيق وخدماته. الترجمة اختيارية،
|
|
38
|
+
وتستخدم المكوّنات النص الافتراضي إذا لم يمرّر التطبيق إعداد ترجمة.
|
|
39
|
+
|
|
40
|
+
معنى ذلك عملياً: إذا استخدمت مكوّناً ولم توفّر الـtoken الذي يطلبه، فلن يفشل البناء بل
|
|
41
|
+
سينهار المكوّن **وقت التشغيل** بخطأ Angular من نوع:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
NullInjectorError: No provider for InjectionToken WORKFLOW_TOOLBAR_PROVIDER!
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
فراجع جدول المكوّنات أدناه لمعرفة الـproviders التي يحتاجها كل مكوّن.
|
|
48
|
+
الـproviders التي يطلبها.
|
|
49
|
+
|
|
50
|
+
ولا يلزم توفير كل الـproviders؛ وفّر فقط ما تطلبه المكوّنات التي تستعملها
|
|
51
|
+
— **وانتبه للاعتمادات غير المباشرة** المعلَّمة بـ¹ و² في الجدول: مكوّن يضمّ مكوّناً آخر
|
|
52
|
+
يرث متطلّباته.
|
|
53
|
+
مثلاً مشروع يستخدم `app-lookup-edit` وحده يحتاج اثنين فقط.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## المكوّنات (٣٧ مكوّناً)
|
|
58
|
+
|
|
59
|
+
العمود الأخير يذكر **الـtokens الإجبارية** لكل مكوّن.
|
|
60
|
+
|
|
61
|
+
### الإدخال والبحث (Lookups)
|
|
62
|
+
|
|
63
|
+
| المكوّن | الـselector | Tokens مطلوبة |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `LookupEditComponent` | `app-lookup-edit`، `lookup-edit` | `LOOKUP_DATA_PROVIDER` + `LOOKUP_GRID_PROVIDER` ¹ |
|
|
66
|
+
| `LookupPopupComponent` | `app-lookup-popup`، `lookup-popup` | `LOOKUP_GRID_PROVIDER` ¹ |
|
|
67
|
+
| `LookupDynamicGridComponent` | `app-lookup-dynamic-grid` | `LOOKUP_GRID_PROVIDER` |
|
|
68
|
+
| `GridLookupComponent` | `app-grid-lookup` | `LOOKUP_GRID_PROVIDER` |
|
|
69
|
+
| `AppSmartLookupComponent` | `app-smart-lookup` | — |
|
|
70
|
+
| `SelectBoxWithLinkComponent` | `app-select-box-with-link` | — |
|
|
71
|
+
|
|
72
|
+
> **¹ اعتماد غير مباشر — انتبه له.** `LookupEditComponent` يحتوي بداخله
|
|
73
|
+
> `LookupPopupComponent`، وهذا بدوره يحتوي `LookupDynamicGridComponent`. فالحقل نفسه يعمل
|
|
74
|
+
> بـ`LOOKUP_DATA_PROVIDER` وحده، لكن **بمجرّد أن يضغط المستخدم زر العدسة** تُنشأ الشبكة
|
|
75
|
+
> وتنهار بـ`NullInjectorError` إن لم يكن `LOOKUP_GRID_PROVIDER` موفَّراً. وفّر الاثنين معاً.
|
|
76
|
+
|
|
77
|
+
### المحرّرات
|
|
78
|
+
|
|
79
|
+
| المكوّن | الـselector | Tokens مطلوبة |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `ImageEditComponent` | `app-image-edit` | — |
|
|
82
|
+
| `FileEditComponent` | `app-file-edit` | `REUSABLE_COMPONENTS_CONFIG` |
|
|
83
|
+
| `DxDateBoxRangeComponent` | `app-dx-date-box-range` | — |
|
|
84
|
+
| `EquationEditorComponent` | `app-equation-editor` | — |
|
|
85
|
+
| `MapLocationPickerComponent` | `app-map-location-picker` | — |
|
|
86
|
+
|
|
87
|
+
### النماذج والتنقّل
|
|
88
|
+
|
|
89
|
+
| المكوّن | الـselector | Tokens مطلوبة |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `FormNavigatorComponent` | `app-form-navigator` | `FORM_NAVIGATOR_ADAPTER` + `LOOKUP_GRID_PROVIDER` + `INFO_USER_PROVIDER` ² (+ `RECORD_SUBSCRIPTION_PROVIDER` عند `allowSubscribe`) |
|
|
92
|
+
| `EditFormButtonsComponent` | `app-edit-form-buttons` | `FORM_NAVIGATOR_ADAPTER` |
|
|
93
|
+
| `SaveOnlyButtonsComponent` | `app-save-only-buttons` | — |
|
|
94
|
+
| `FilterPanelComponent` | `app-filter-panel` | `LOOKUP_DATA_PROVIDER` + `LOOKUP_GRID_PROVIDER` ³ |
|
|
95
|
+
| `GroupingLevelComponent` | `app-grouping-level` | `FORM_NAVIGATOR_ADAPTER` |
|
|
96
|
+
| `FnIconComponent` | `app-fn-icon` | — |
|
|
97
|
+
|
|
98
|
+
> **² `FormNavigatorComponent` يضمّ ثلاثة مكوّنات أخرى.** `LookupPopupComponent`
|
|
99
|
+
> و`InfoPopupComponent` **يُنشآن مع كل متنقّل بلا شرط**، فـ`LOOKUP_GRID_PROVIDER`
|
|
100
|
+
> و`INFO_USER_PROVIDER` **إجباريان** لأي شاشة تستعمل المتنقّل — لا خيار فيهما.
|
|
101
|
+
> أمّا `SubscribeButtonComponent` فمحاط بـ`@if (allowSubscribe && …)`، فـ
|
|
102
|
+
> `RECORD_SUBSCRIPTION_PROVIDER` لا يلزم إلّا إذا فعّلت `allowSubscribe`.
|
|
103
|
+
|
|
104
|
+
> **³ `FilterPanelComponent` يضمّ `LookupEditComponent` و`GridLookupComponent` داخل `*dxTemplate`.**
|
|
105
|
+
> فلا يلزمان إلّا إذا كانت لوحة التصفية تحوي فلاتر من نوع lookup — وهذا يُحسم وقت
|
|
106
|
+
> التشغيل حسب الميتاداتا، فالأمن توفيرهما.
|
|
107
|
+
|
|
108
|
+
### المرفقات
|
|
109
|
+
|
|
110
|
+
| المكوّن | الـselector | Tokens مطلوبة |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `FileManagerComponent` | `app-file-manager` | `FILE_MANAGER_PROVIDER` + `FORM_NAVIGATOR_ADAPTER` |
|
|
113
|
+
|
|
114
|
+
### سير العمل (Workflow)
|
|
115
|
+
|
|
116
|
+
| المكوّن | الـselector | Tokens مطلوبة |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `WorkflowActionsComponent` | `app-workflow-actions` | `WORKFLOW_ACTIONS_PROVIDER` + `FORM_NAVIGATOR_ADAPTER` |
|
|
119
|
+
| `WorkflowToolbarComponent` | `app-workflow-toolbar` | `WORKFLOW_TOOLBAR_PROVIDER` + `FORM_NAVIGATOR_ADAPTER` |
|
|
120
|
+
|
|
121
|
+
### الأبعاد المالية
|
|
122
|
+
|
|
123
|
+
| المكوّن | الـselector | Tokens مطلوبة |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `FinancialDimensionsPopupComponent` | `app-financial-dimensions-popup` | `FINANCIAL_DIMENSIONS_PROVIDER` |
|
|
126
|
+
| `FinancialDimensionValuesComponent` | `app-financial-dimension-values` | `FINANCIAL_DIMENSION_VALUES_PROVIDER` + `LOOKUP_GRID_PROVIDER` + `FORM_NAVIGATOR_ADAPTER` |
|
|
127
|
+
|
|
128
|
+
### الصلاحيات
|
|
129
|
+
|
|
130
|
+
| المكوّن | الـselector | Tokens مطلوبة |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `PermissionSelectorComponent` | `app-permission-selector` | — |
|
|
133
|
+
| `PermissionSelectorItemComponent` | `app-permission-selector-item` | — |
|
|
134
|
+
|
|
135
|
+
### السجلّات والإشعارات
|
|
136
|
+
|
|
137
|
+
| المكوّن | الـselector | Tokens مطلوبة |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `RecordChangeLogPopupComponent` | `app-record-change-log-popup` | `RECORD_CHANGE_LOG_PROVIDER` |
|
|
140
|
+
| `SubscribeButtonComponent` | `app-subscribe-button` | `RECORD_SUBSCRIPTION_PROVIDER` |
|
|
141
|
+
| `InfoPopupComponent` | `app-info-popup` | `INFO_USER_PROVIDER` |
|
|
142
|
+
| `MessageContentPopupComponent` | `app-message-content-popup` | — |
|
|
143
|
+
| `JournalEntryViewerComponent` | `app-journal-entry-viewer` | `JOURNAL_ENTRY_VIEWER_PROVIDER` |
|
|
144
|
+
| `QueryActionsToolbarComponent` | `app-query-actions-toolbar` | — |
|
|
145
|
+
| `ReceiptPreviewComponent` | `app-receipt-preview` | `REUSABLE_COMPONENTS_CONFIG` (`isArabic`) |
|
|
146
|
+
| `ReportWorkflowActionsComponent` | `app-report-workflow-actions` | — |
|
|
147
|
+
| `ReversalDatePopupComponent` | `app-reversal-date-popup` | `REUSABLE_COMPONENTS_CONFIG` (`translate`) |
|
|
148
|
+
| `WorkflowConfirmDialogComponent` | `app-workflow-confirm-dialog` | — |
|
|
149
|
+
|
|
150
|
+
### التخطيط
|
|
151
|
+
|
|
152
|
+
| المكوّن | الـselector | Tokens مطلوبة |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `AppToolbarComponent` | `app-toolbar` | — |
|
|
155
|
+
| `AppToolbarItemComponent` | `app-toolbar-item` | — |
|
|
156
|
+
| `AppBorderWrapperComponent` | `app-border-wrapper` | — |
|
|
157
|
+
|
|
158
|
+
### توجيهات (Directives)
|
|
159
|
+
|
|
160
|
+
| الاسم | الـselector |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `DxFormItemsOutletDirective` | يُستخدم داخل `app-filter-panel` |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## مرجع الـProviders (١٤)
|
|
167
|
+
|
|
168
|
+
### ١. `REUSABLE_COMPONENTS_CONFIG` — إعدادات عامة
|
|
169
|
+
|
|
170
|
+
عنوان الـAPI. يستخدمه `FileEditComponent`.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { provideReusableComponents } from '@devhouse.int/angular-reusable-components';
|
|
174
|
+
|
|
175
|
+
provideReusableComponents({ apiUrl: 'https://api.example.com/api' })
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
حقول اختيارية: `translate(key, fallback)` و`isArabic()`. أولوية الترجمة في كل المكوّنات
|
|
179
|
+
(بايب `localize` الموحّد): `config.translate` ← `FORM_NAVIGATOR_ADAPTER.translate` ← النصّ الافتراضي.
|
|
180
|
+
|
|
181
|
+
> `ReportWorkflowActionsComponent` لا يحمل أي معرّفات حالات أو إجراءات افتراضية (تخصّ دورة عمل
|
|
182
|
+
> كل مستأجر): مرّر `draftStatusId` و`approvedStatusId` و`archivedStatusId` و`approveActionId` و
|
|
183
|
+
> `unapproveActionId` و`archiveActionId` و`unarchiveActionId`، وإلا بقيت الأزرار المقابلة معطّلة.
|
|
184
|
+
|
|
185
|
+
### ٢. `LOOKUP_DATA_PROVIDER` — بيانات الـlookup
|
|
186
|
+
|
|
187
|
+
يستخدمه `LookupEditComponent`. كل دالة تقبل الإرجاع بـ`Promise` أو `Observable` أو قيمة مباشرة.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { provideLookup } from '@devhouse.int/angular-reusable-components';
|
|
191
|
+
|
|
192
|
+
provideLookup({
|
|
193
|
+
// يحوّل مفتاحاً واحداً إلى صف للعرض (أو null إن لم يوجد)
|
|
194
|
+
resolve: (ctx, key) => api.resolve(ctx.lookupId, key, ctx.parameters),
|
|
195
|
+
// بحث نصّي — يُرجع { items, columns?, title? }
|
|
196
|
+
search: (ctx, query) => api.search(ctx.lookupId, query, ctx.parameters),
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`ctx` من نوع `LookupContext` ويحمل: `lookupId` · `parameters` · `lookupParameterSource` ·
|
|
201
|
+
`lookupParameters`. والصف المُرجَع `LookupItem` = `{ id, value, label, rowData }`.
|
|
202
|
+
|
|
203
|
+
### ٣. `LOOKUP_GRID_PROVIDER` — الشبكات المعتمدة على الميتاداتا
|
|
204
|
+
|
|
205
|
+
يستخدمه `LookupDynamicGridComponent` و`GridLookupComponent` و`FinancialDimensionValuesComponent`.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
{
|
|
209
|
+
provide: LOOKUP_GRID_PROVIDER,
|
|
210
|
+
useValue: {
|
|
211
|
+
getSysLookup: (id) => Observable<any>, // تعريف الـlookup من الميتاداتا
|
|
212
|
+
createDataSource: (...args) => Promise<any>, // DataSource/CustomStore للشبكة
|
|
213
|
+
toCamelCase: (value: string) => string, // تطبيع أسماء الأعمدة
|
|
214
|
+
createGridColumns: (...args) => any[], // بناء أعمدة DevExtreme
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
تُصدَّر معه أدوات مساعدة: `LookupValueType` (تعداد أنواع القيم) و`LINQ_SELECT_COMMAND_TYPE`
|
|
220
|
+
و`lookupValueToArray()`.
|
|
221
|
+
|
|
222
|
+
### ٤. `FORM_NAVIGATOR_ADAPTER` — الأكثر استعمالاً ⭐
|
|
223
|
+
|
|
224
|
+
**مطلوب لسبعة مكوّنات**: `FormNavigatorComponent` · `EditFormButtonsComponent` ·
|
|
225
|
+
`FileManagerComponent` · `FinancialDimensionValuesComponent` · `GroupingLevelComponent` ·
|
|
226
|
+
`WorkflowActionsComponent` · `WorkflowToolbarComponent`.
|
|
227
|
+
|
|
228
|
+
هذا هو الجسر الذي يستبدل خدمات الصلاحيات والترجمة والإشعارات والتحميل في المشروع المستضيف:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
{
|
|
232
|
+
provide: FORM_NAVIGATOR_ADAPTER,
|
|
233
|
+
useValue: {
|
|
234
|
+
// الصلاحيات — تقود إظهار/تعطيل أزرار المتنقّل
|
|
235
|
+
permissions$: Observable<FormNavigatorPermissions | null>,
|
|
236
|
+
loadPermissions: () => void,
|
|
237
|
+
|
|
238
|
+
// الترجمة
|
|
239
|
+
translate: (key: string, fallback: string) => string,
|
|
240
|
+
messages: { // نصوص جاهزة تستخدمها المكوّنات مباشرةً
|
|
241
|
+
Load_Data_ERROR, NOT_FOUND, NO_PERMISSION, SAVE_ERROR, SAVE_SUCCESS,
|
|
242
|
+
DELETE_ERROR, DUPLICATE_ERROR, DUPLICATE_SUCCESS, VALIDATION_REQUIRED,
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// التنبيهات والتأكيدات
|
|
246
|
+
notify: (type: 'success'|'error'|'info'|'warning', message: string) => void,
|
|
247
|
+
confirm: (type: 'delete'|'duplicate'|'unsaved') => Promise<boolean>,
|
|
248
|
+
|
|
249
|
+
// تتبّع تحميل السجل (لانتظار اكتمال الـlookups قبل اعتبار السجل جاهزاً)
|
|
250
|
+
beginRecordLoad: () => number,
|
|
251
|
+
waitForLookups: (loadId: number) => Promise<void>,
|
|
252
|
+
isRecordLoadActive: (loadId: number) => boolean,
|
|
253
|
+
|
|
254
|
+
// لوحة التحميل المشتركة
|
|
255
|
+
showLoading: (message: string) => void,
|
|
256
|
+
hideLoading: () => void,
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`FormNavigatorPermissions` يضمّ ١٥ صلاحية اختيارية: `canInsert` · `canUpdate` · `canOpen` ·
|
|
262
|
+
`canDelete` · `canPrint` · `canExecute` · `canExport` · `canImport` · `canQuery` ·
|
|
263
|
+
`canPrintreport` · `canDesignreport` · `canAssign` · `canShare` · `canBrowse` · `canLogin`.
|
|
264
|
+
|
|
265
|
+
### ٥. `FILE_MANAGER_PROVIDER` — مدير المرفقات
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
{
|
|
269
|
+
provide: FILE_MANAGER_PROVIDER,
|
|
270
|
+
useValue: {
|
|
271
|
+
// عنوان RemoteFileSystemProvider الخاص بـDevExtreme
|
|
272
|
+
getFileSystemEndpoint: (attachmentSourceId, entityKey) => string,
|
|
273
|
+
|
|
274
|
+
// ⚠️ مهم: نقل DevExtreme لا يمرّ على HttpInterceptors، فترويسات الهوية
|
|
275
|
+
// (مثل X-Tenant-Domain) يجب تمريرها هنا صراحةً
|
|
276
|
+
fileSystemRequestHeaders: () => Record<string, string>,
|
|
277
|
+
|
|
278
|
+
createTagsStore: () => any, // { id, name_Ar }
|
|
279
|
+
addURL: (data) => Observable<unknown>,
|
|
280
|
+
uploadManual: (...) => Observable<any>,
|
|
281
|
+
updateFileMetadataWithFile: (...) => Observable<any>,
|
|
282
|
+
downloadFile: (fileId) => Observable<Blob>,
|
|
283
|
+
getAttachmentTags: (attachmentId) => Observable<any[]>,
|
|
284
|
+
setAttachmentTagsById: (attachmentId, tags) => Observable<unknown>,
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### ٦. `WORKFLOW_ACTIONS_PROVIDER` — شريط إجراءات سير العمل
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
{
|
|
293
|
+
provide: WORKFLOW_ACTIONS_PROVIDER,
|
|
294
|
+
useValue: {
|
|
295
|
+
// كل ما يحتاجه الشريط في نداء واحد (يرجع 200 دائماً، لا 404)
|
|
296
|
+
getDocumentContext: (documentTypeId, documentId) => Observable<WorkflowDocumentContext>,
|
|
297
|
+
applyTransition: (instanceId, input) => Observable<WorkflowInstanceResult>,
|
|
298
|
+
// يُطلَق عند تغيّر سير العمل من تبويب/شاشة أخرى فيعيد الشريط التحميل
|
|
299
|
+
workflowUpdates$: Observable<{ documentTypeId, documentId }>,
|
|
300
|
+
isArabic: () => boolean,
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
تُصدَّر معه دالتان لتلوين الأزرار حسب نوع الانتقال
|
|
306
|
+
(`APPROVE` أخضر · `REJECT` أحمر · `ESCALATE` برتقالي):
|
|
307
|
+
`workflowTransitionColor()` و`workflowTransitionButtonClass()`.
|
|
308
|
+
|
|
309
|
+
### ٧. `WORKFLOW_TOOLBAR_PROVIDER` — شريط أدوات سير العمل
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
{
|
|
313
|
+
provide: WORKFLOW_TOOLBAR_PROVIDER,
|
|
314
|
+
useValue: {
|
|
315
|
+
getAvailableWorkflowActions: (workflowId, statusId) => Observable<any[]>,
|
|
316
|
+
// يحوّل الإجراءات الخام إلى عناصر شريط:
|
|
317
|
+
// { text, iconClass, btnClass, hint, onClick, disabled?, visible? }
|
|
318
|
+
mapWorkflowActionsItems: (actions, applyWorkflowCallback) => any[],
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### ٨. `FINANCIAL_DIMENSIONS_PROVIDER` — نافذة الأبعاد المالية
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
{
|
|
327
|
+
provide: FINANCIAL_DIMENSIONS_PROVIDER,
|
|
328
|
+
useValue: {
|
|
329
|
+
getDimensionsByAccount: (accountId, combinationId?) => Promise<FinancialDimensionVm[]>,
|
|
330
|
+
getAllDimensions: () => Promise<FinancialDimensionVm[]>,
|
|
331
|
+
getCombinationSelections: (dimensions, combinationId) => Promise<DimensionSelectionsMap>,
|
|
332
|
+
buildPayload: (dimensions, selections, accountId) => any,
|
|
333
|
+
createOrGetCombination: (payload) => Promise<{ id, display_Value }>,
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### ٩. `FINANCIAL_DIMENSION_VALUES_PROVIDER` — شبكة قيم الأبعاد
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
{
|
|
342
|
+
provide: FINANCIAL_DIMENSION_VALUES_PROVIDER,
|
|
343
|
+
useValue: {
|
|
344
|
+
dimensionLookupId: any, // معرّف lookup الأبعاد
|
|
345
|
+
dimensionValueLookupId: any, // معرّف lookup القيم
|
|
346
|
+
fieldDataParameterSource: string | number,
|
|
347
|
+
loadValues: (options) => Observable<any>,
|
|
348
|
+
loadDimensions: (options) => Observable<any>,
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### ١٠. `RECORD_CHANGE_LOG_PROVIDER` — سجلّ تغييرات السجل
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
{
|
|
357
|
+
provide: RECORD_CHANGE_LOG_PROVIDER,
|
|
358
|
+
useValue: {
|
|
359
|
+
getLog: (tableName: string, recordId: string) => Observable<RecordChangeLogResult>,
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
`RecordChangeLogResult` = `{ table_Name, record_Id, created_By, created_On, modified_By,
|
|
365
|
+
modified_On, changes[] }`، وكل عنصر في `changes` = `{ column_Name, old_Value, new_Value,
|
|
366
|
+
changed_On, changed_By }`.
|
|
367
|
+
|
|
368
|
+
### ١١. `RECORD_SUBSCRIPTION_PROVIDER` — الاشتراك في إشعارات سجل
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
{
|
|
372
|
+
provide: RECORD_SUBSCRIPTION_PROVIDER,
|
|
373
|
+
useValue: {
|
|
374
|
+
isSubscribed: (recordId) => Observable<boolean>,
|
|
375
|
+
getMine: () => Observable<RecordSubscriptionItem[]>,
|
|
376
|
+
subscribe: (recordId, prefs: SubscriptionPrefs) => Observable<unknown>,
|
|
377
|
+
unsubscribe: (recordId) => Observable<unknown>,
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`SubscriptionPrefs` = أحداث (`notifyOnUpdate` · `notifyOnStatusChange` · `notifyOnDelete`)
|
|
383
|
+
وقنوات (`channelInApp` · `channelEmail` · `channelWhatsapp` · `channelSms`).
|
|
384
|
+
|
|
385
|
+
### ١٢. `INFO_USER_PROVIDER` — بيانات المستخدم لنافذة المعلومات
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
{
|
|
389
|
+
provide: INFO_USER_PROVIDER,
|
|
390
|
+
useValue: { getById: (id) => Observable<unknown> | Promise<unknown> },
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### ١٣. `LINQ_SELECT_COMMAND_TYPE` — ثابت لا token
|
|
395
|
+
|
|
396
|
+
ليس injection token بل **ثابت مُصدَّر** قيمته `2`، يُستخدم عند التفريق بين أوامر
|
|
397
|
+
الـlookup من نوع LINQ ومن نوع SQL.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## مثال توصيل كامل
|
|
402
|
+
|
|
403
|
+
مشروع يستخدم المتنقّل والـlookups وشريط سير العمل:
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
// app.config.ts
|
|
407
|
+
import { ApplicationConfig } from '@angular/core';
|
|
408
|
+
import {
|
|
409
|
+
provideReusableComponents,
|
|
410
|
+
provideLookup,
|
|
411
|
+
FORM_NAVIGATOR_ADAPTER,
|
|
412
|
+
LOOKUP_GRID_PROVIDER,
|
|
413
|
+
WORKFLOW_TOOLBAR_PROVIDER,
|
|
414
|
+
} from '@devhouse.int/angular-reusable-components';
|
|
415
|
+
|
|
416
|
+
export const appConfig: ApplicationConfig = {
|
|
417
|
+
providers: [
|
|
418
|
+
provideReusableComponents({ apiUrl: environment.apiUrl }),
|
|
419
|
+
|
|
420
|
+
provideLookup({
|
|
421
|
+
resolve: (ctx, key) => inject(LookupApi).resolve(ctx.lookupId, key, ctx.parameters),
|
|
422
|
+
search: (ctx, query) => inject(LookupApi).search(ctx.lookupId, query, ctx.parameters),
|
|
423
|
+
}),
|
|
424
|
+
|
|
425
|
+
// خدمة موجودة عندك بنفس الشكل؟ استخدم useExisting بدل useValue
|
|
426
|
+
{ provide: LOOKUP_GRID_PROVIDER, useExisting: MyLookupGridService },
|
|
427
|
+
{ provide: WORKFLOW_TOOLBAR_PROVIDER, useExisting: MyWorkflowActionService },
|
|
428
|
+
|
|
429
|
+
{
|
|
430
|
+
provide: FORM_NAVIGATOR_ADAPTER,
|
|
431
|
+
useFactory: () => {
|
|
432
|
+
const perms = inject(PermissionService);
|
|
433
|
+
const i18n = inject(TranslateService);
|
|
434
|
+
const loader = inject(LoadingService);
|
|
435
|
+
return {
|
|
436
|
+
permissions$: perms.permissions$,
|
|
437
|
+
loadPermissions: () => perms.load(),
|
|
438
|
+
translate: (k, fb) => i18n.get(k) ?? fb,
|
|
439
|
+
messages: MY_MESSAGES,
|
|
440
|
+
notify: (type, msg) => notify(msg, type),
|
|
441
|
+
confirm: (type) => myConfirm(type),
|
|
442
|
+
beginRecordLoad: () => Date.now(),
|
|
443
|
+
waitForLookups: async () => {},
|
|
444
|
+
isRecordLoadActive: () => false,
|
|
445
|
+
showLoading: (m) => loader.show(m),
|
|
446
|
+
hideLoading: () => loader.hide(),
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
};
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
ثم في المكوّن:
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
import { LookupEditComponent } from '@devhouse.int/angular-reusable-components';
|
|
458
|
+
|
|
459
|
+
@Component({ imports: [LookupEditComponent], /* … */ })
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
```html
|
|
463
|
+
<app-lookup-edit [lookupId]="'customers'" caption="العميل" [(idValue)]="customerId" />
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## نموذج التحرير داخل نافذة منبثقة (Embedded form)
|
|
469
|
+
|
|
470
|
+
منذ **0.4.0** يستطيع `EditFormButtonsComponent` أن يعمل داخل نافذة منبثقة فوق الشبكة بدل
|
|
471
|
+
فتح تبويب متصفّح جديد. وضع المسار (route) **لم يتغيّر إطلاقاً**: كل المُدخلات الجديدة
|
|
472
|
+
اختيارية، وعدم ضبطها يعني السلوك القديم حرفياً.
|
|
473
|
+
|
|
474
|
+
```html
|
|
475
|
+
<app-edit-form-buttons
|
|
476
|
+
[embedded]="true"
|
|
477
|
+
[embeddedMode]="'edit'"
|
|
478
|
+
[embeddedId]="selectedId"
|
|
479
|
+
(saved)="grid.refresh()"
|
|
480
|
+
(closed)="popupVisible = false"
|
|
481
|
+
/>
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
| | |
|
|
485
|
+
|---|---|
|
|
486
|
+
| `embedded` | `true` = داخل نافذة، `false` (الافتراضي) = وضع المسار |
|
|
487
|
+
| `embeddedMode` | `'new'` أو `'edit'` |
|
|
488
|
+
| `embeddedId` | معرّف السجل عند التعديل |
|
|
489
|
+
| `(saved)` | يُطلَق بعد كل حفظ ناجح — بديل `postMessage` |
|
|
490
|
+
| `(closed)` | يُطلَق عند انتهاء الشاشة — المضيف يغلق النافذة |
|
|
491
|
+
|
|
492
|
+
وللشاشة المستضيفة يوجد العقد `EmbeddedFormHost` وحارس النوع `isEmbeddedFormHost()`،
|
|
493
|
+
فتستطيع الشبكة المضيفة أن تتحقّق قبل إنشاء المكوّن وتتراجع إلى `window.open` إن لم تكن
|
|
494
|
+
الشاشة محوَّلة بعد.
|
|
495
|
+
|
|
496
|
+
**مهم:** أغلِق النافذة عبر `requestClose()` لا بإخفائها مباشرةً — كي يمرّ الإغلاق بمسار
|
|
497
|
+
الشاشة الطبيعي (بما فيه تأكيد التغييرات غير المحفوظة).
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## البناء
|
|
502
|
+
|
|
503
|
+
```sh
|
|
504
|
+
npm install
|
|
505
|
+
npm run build # ng-packagr → dist/
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## ملاحظات
|
|
511
|
+
|
|
512
|
+
- `MapLocationPickerComponent` يحتاج `leaflet` وملفات أنماطه في المشروع المستضيف.
|
|
513
|
+
- نقل DevExtreme الداخلي (`RemoteFileSystemProvider` في مدير المرفقات) **لا يمرّ على
|
|
514
|
+
`HttpInterceptors`** الخاصة بـAngular — مرّر ترويسات الهوية عبر
|
|
515
|
+
`fileSystemRequestHeaders()`.
|
|
516
|
+
- معظم النصوص الافتراضية عربية، وتُستبدَل عبر `translate()` و`messages` في
|
|
517
|
+
`FORM_NAVIGATOR_ADAPTER`.
|