@kernhq/module-inventory 0.1.2 → 0.2.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.
Files changed (103) hide show
  1. package/README.md +55 -9
  2. package/dist/contract/capabilities.d.ts +49 -0
  3. package/dist/contract/capabilities.d.ts.map +1 -0
  4. package/dist/contract/capabilities.js +52 -0
  5. package/dist/contract/capabilities.js.map +1 -0
  6. package/dist/contract/events.d.ts +33 -0
  7. package/dist/contract/events.d.ts.map +1 -0
  8. package/dist/contract/events.js +22 -0
  9. package/dist/contract/events.js.map +1 -0
  10. package/dist/contract/index.d.ts +15 -0
  11. package/dist/contract/index.d.ts.map +1 -0
  12. package/dist/contract/index.js +15 -0
  13. package/dist/contract/index.js.map +1 -0
  14. package/dist/contract/models.d.ts +146 -0
  15. package/dist/contract/models.d.ts.map +1 -0
  16. package/dist/contract/models.js +107 -0
  17. package/dist/contract/models.js.map +1 -0
  18. package/dist/contract/permissions.d.ts +22 -0
  19. package/dist/contract/permissions.d.ts.map +1 -0
  20. package/dist/contract/permissions.js +26 -0
  21. package/dist/contract/permissions.js.map +1 -0
  22. package/dist/{contract.d.ts → contract/router.d.ts} +24 -85
  23. package/dist/contract/router.d.ts.map +1 -0
  24. package/dist/contract/router.js +42 -0
  25. package/dist/contract/router.js.map +1 -0
  26. package/dist/contract/settings.d.ts +18 -0
  27. package/dist/contract/settings.d.ts.map +1 -0
  28. package/dist/contract/settings.js +29 -0
  29. package/dist/contract/settings.js.map +1 -0
  30. package/dist/server/index.d.ts +4 -1
  31. package/dist/server/index.d.ts.map +1 -1
  32. package/dist/server/index.js +9 -13
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/server/{_impl.d.ts → router.d.ts} +33 -8
  35. package/dist/server/{_impl.d.ts.map → router.d.ts.map} +1 -1
  36. package/dist/server/router.js +83 -0
  37. package/dist/server/router.js.map +1 -0
  38. package/dist/server/schema.d.ts +25 -11
  39. package/dist/server/schema.d.ts.map +1 -1
  40. package/dist/server/schema.js +28 -10
  41. package/dist/server/schema.js.map +1 -1
  42. package/dist/server/services/assets.d.ts +73 -0
  43. package/dist/server/services/assets.d.ts.map +1 -0
  44. package/dist/server/services/assets.js +261 -0
  45. package/dist/server/services/assets.js.map +1 -0
  46. package/dist/server/services/index.d.ts +10 -0
  47. package/dist/server/services/index.d.ts.map +1 -0
  48. package/dist/server/services/index.js +15 -0
  49. package/dist/server/services/index.js.map +1 -0
  50. package/dist/server/services/notify.d.ts +63 -0
  51. package/dist/server/services/notify.d.ts.map +1 -0
  52. package/dist/server/services/notify.js +105 -0
  53. package/dist/server/services/notify.js.map +1 -0
  54. package/migrations/0000_init.sql +12 -3
  55. package/migrations/0001_rls.sql +24 -0
  56. package/migrations/meta/0000_snapshot.json +40 -13
  57. package/migrations/meta/_journal.json +2 -2
  58. package/package.json +5 -4
  59. package/src/client/api.ts +1 -1
  60. package/src/client/components/AssetFormDialog.svelte +130 -47
  61. package/src/client/i18n.ts +11 -166
  62. package/src/client/index.ts +8 -1
  63. package/src/client/messages.test.ts +169 -0
  64. package/src/client/messages.ts +399 -0
  65. package/src/client/mock.test.ts +161 -0
  66. package/src/client/mock.ts +267 -45
  67. package/src/client/module.ts +22 -2
  68. package/src/client/pages/AssetsPage.svelte +358 -137
  69. package/src/client/permissions.ts +1 -1
  70. package/src/client/price.test.ts +106 -0
  71. package/src/client/price.ts +135 -0
  72. package/src/client/query.test.ts +58 -0
  73. package/src/client/query.ts +15 -2
  74. package/src/client/settings/GeneralSettings.svelte +0 -0
  75. package/src/client/settings/core-api.ts +32 -0
  76. package/src/client/widgets/OverviewWidget.svelte +16 -3
  77. package/src/contract/capabilities.ts +55 -0
  78. package/src/contract/events.ts +34 -0
  79. package/src/contract/index.ts +15 -0
  80. package/src/contract/models.ts +123 -0
  81. package/src/contract/permissions.ts +26 -0
  82. package/src/contract/router.ts +46 -0
  83. package/src/contract/settings.ts +30 -0
  84. package/src/module.test.ts +140 -7
  85. package/src/server/index.ts +16 -13
  86. package/src/server/inventory.int.test.ts +819 -0
  87. package/src/server/migrations.test.ts +138 -0
  88. package/src/server/router.ts +118 -0
  89. package/src/server/schema.ts +27 -10
  90. package/src/server/services/assets.ts +368 -0
  91. package/src/server/services/index.ts +23 -0
  92. package/src/server/services/notify.ts +151 -0
  93. package/tsconfig.base.json +22 -0
  94. package/tsconfig.client.json +1 -1
  95. package/tsconfig.json +1 -1
  96. package/vitest.config.ts +18 -3
  97. package/dist/contract.d.ts.map +0 -1
  98. package/dist/contract.js +0 -119
  99. package/dist/contract.js.map +0 -1
  100. package/dist/server/_impl.js +0 -204
  101. package/dist/server/_impl.js.map +0 -1
  102. package/src/contract.ts +0 -143
  103. package/src/server/_impl.ts +0 -275
@@ -0,0 +1,399 @@
1
+ import type { Message } from '@kernhq/ui'
2
+
3
+ /**
4
+ * This module's strings, in every locale the platform ships.
5
+ *
6
+ * **Data only, and deliberately so:** this file imports nothing but a type, which is what lets a
7
+ * test load the bundles and check them. `i18n.ts` next door pulls in `@kernhq/ui` for `scopedT`,
8
+ * and that entry point reaches the Svelte components — so anything importing it drags a compiler
9
+ * into whatever is doing the importing. Keys, plural forms and placeholders are checkable facts;
10
+ * keeping them behind that import made them uncheckable.
11
+ *
12
+ * A module ships separately from the app, so Paraglide — which compiles only the app's
13
+ * `messages/*.json` — cannot see these. The shell merges them into the framework's message runtime
14
+ * when it registers the module. Keys are namespaced by module id, which is what keeps two modules
15
+ * from colliding in that one map.
16
+ *
17
+ * A **counted** message is not a string with `{n}` in it: give it a map of CLDR plural category to
18
+ * string and `t(key, { n })` picks the form. English has two and Arabic has six, and which one
19
+ * applies is `Intl.PluralRules`' answer rather than yours.
20
+ *
21
+ * Words every module needs and none owns — Save, Cancel, Retry — come from the framework's `common`
22
+ * bundle (`t('common.save')`). Do not copy them here; six translations of "Save" drift apart.
23
+ */
24
+
25
+ export const en: Record<string, Message> = {
26
+ 'inventory.nav': 'Inventory',
27
+ 'inventory.title': 'Assets',
28
+ 'inventory.empty': 'No assets yet',
29
+ 'inventory.empty_desc': 'Add the first laptop, phone or desk — anything the company owns.',
30
+ 'inventory.new': 'New asset',
31
+ 'inventory.edit': 'Edit asset',
32
+ 'inventory.code': 'Asset tag',
33
+ 'inventory.name': 'Name',
34
+ 'inventory.name_placeholder': 'MacBook Pro 14", office chair…',
35
+ 'inventory.description': 'Description',
36
+ 'inventory.status': 'Status',
37
+ 'inventory.serial_number': 'Serial number',
38
+ 'inventory.location': 'Location',
39
+ 'inventory.purchased_on': 'Purchase date',
40
+ 'inventory.purchased_from': 'Purchased from',
41
+ 'inventory.price': 'Price',
42
+ 'inventory.warranty_until': 'Warranty until',
43
+ 'inventory.status_in_stock': 'In stock',
44
+ 'inventory.status_assigned': 'Assigned',
45
+ 'inventory.status_reserved': 'Reserved',
46
+ 'inventory.status_under_repair': 'Under repair',
47
+ 'inventory.status_lost': 'Lost',
48
+ 'inventory.status_retired': 'Retired',
49
+ 'inventory.currency': 'Currency',
50
+ 'inventory.price_invalid': 'Enter an amount like {example}',
51
+ 'inventory.count': { one: '{n} asset', other: '{n} assets' },
52
+ 'inventory.count_showing': { one: 'Showing {n} asset', other: 'Showing {n} assets' },
53
+ 'inventory.search_placeholder': 'Search by name, tag or serial…',
54
+ 'inventory.filter_all_statuses': 'All statuses',
55
+ 'inventory.archived': 'Archived',
56
+ 'inventory.restore': 'Restore',
57
+ 'inventory.show_archived': 'Show archived',
58
+ 'inventory.load_more': 'Load more',
59
+ 'inventory.row_actions': 'Actions for {name}',
60
+ 'inventory.archive_title': 'Archive {name}?',
61
+ 'inventory.archive_body':
62
+ 'It stops appearing in the list and keeps its tag. Nothing is deleted, and you can restore it at any time.',
63
+ 'inventory.archived_toast': '{name} archived',
64
+ 'inventory.restored_toast': '{name} restored',
65
+ 'inventory.created_toast': '{name} added',
66
+ 'inventory.updated_toast': '{name} updated',
67
+ 'inventory.load_error': 'The assets could not be loaded',
68
+ 'inventory.no_matches': 'Nothing matched',
69
+ 'inventory.no_matches_desc': 'No asset matches this search or these filters.',
70
+ 'inventory.clear_filters': 'Clear filters',
71
+ 'inventory.settings_general': 'General',
72
+ 'inventory.settings_general_desc': 'How this workspace numbers the things it owns.',
73
+ 'inventory.settings_numbering': 'Asset tags',
74
+ 'inventory.settings_numbering_desc':
75
+ 'The shape of every tag issued from now on. Tags already on a sticker keep the shape they were issued with.',
76
+ 'inventory.settings_prefix': 'Prefix',
77
+ 'inventory.settings_prefix_hint': 'Up to 8 characters, or none for numbers alone.',
78
+ 'inventory.settings_prefix_too_long': 'Use 8 characters or fewer',
79
+ 'inventory.settings_pad': 'Digits',
80
+ 'inventory.settings_pad_hint': 'How many digits the number is padded out to.',
81
+ 'inventory.settings_pad_invalid': 'Use a whole number from 1 to 10',
82
+ // "Example", not "Next tag": the counter is a row in the module's own table rather than a
83
+ // setting, so this page cannot know what the next tag will be and must not imply that it does.
84
+ 'inventory.settings_preview': 'Example tag',
85
+ 'inventory.settings_saved': 'Asset tag settings saved',
86
+ 'inventory.settings_save_error': 'The settings could not be saved',
87
+ 'inventory.settings_error': 'The settings could not be loaded',
88
+ 'inventory.settings_readonly': 'You can read these settings but not change them.',
89
+ 'inventory.settings_not_enabled': 'Inventory is not enabled in this workspace',
90
+ 'inventory.widget_title': 'Recent assets',
91
+ 'inventory.widget_desc': 'The most recently added assets in this workspace.',
92
+ }
93
+
94
+ export type InventoryMessageKey = keyof typeof en
95
+
96
+ export const fa: Record<string, Message> = {
97
+ 'inventory.nav': 'اموال',
98
+ 'inventory.title': 'اقلام',
99
+ 'inventory.empty': 'هنوز قلمی ثبت نشده است',
100
+ 'inventory.empty_desc': 'اولین لپ‌تاپ، گوشی یا میز را اضافه کنید — هر چیزی که شرکت دارد.',
101
+ 'inventory.new': 'قلم جدید',
102
+ 'inventory.edit': 'ویرایش قلم',
103
+ 'inventory.code': 'کد قلم',
104
+ 'inventory.name': 'نام',
105
+ 'inventory.name_placeholder': 'مک‌بوک پرو ۱۴ اینچ، صندلی اداری…',
106
+ 'inventory.description': 'توضیحات',
107
+ 'inventory.status': 'وضعیت',
108
+ 'inventory.serial_number': 'شماره سریال',
109
+ 'inventory.location': 'محل استقرار',
110
+ 'inventory.purchased_on': 'تاریخ خرید',
111
+ 'inventory.purchased_from': 'خریداری از',
112
+ 'inventory.price': 'قیمت',
113
+ 'inventory.warranty_until': 'گارانتی تا',
114
+ 'inventory.status_in_stock': 'در انبار',
115
+ 'inventory.status_assigned': 'تحویل‌شده',
116
+ 'inventory.status_reserved': 'رزروشده',
117
+ 'inventory.status_under_repair': 'در تعمیر',
118
+ 'inventory.status_lost': 'مفقود',
119
+ 'inventory.status_retired': 'اسقاط‌شده',
120
+ 'inventory.currency': 'واحد پول',
121
+ 'inventory.price_invalid': 'مبلغی مانند {example} وارد کنید',
122
+ 'inventory.count': { one: '{n} قلم', other: '{n} قلم' },
123
+ 'inventory.count_showing': { one: 'نمایش {n} قلم', other: 'نمایش {n} قلم' },
124
+ 'inventory.search_placeholder': 'جستجو بر اساس نام، کد یا سریال…',
125
+ 'inventory.filter_all_statuses': 'همه وضعیت‌ها',
126
+ 'inventory.archived': 'بایگانی‌شده',
127
+ 'inventory.restore': 'بازگردانی',
128
+ 'inventory.show_archived': 'نمایش بایگانی‌شده‌ها',
129
+ // The shell's own catalogue says «بارگذاری بیشتر» for this English string (`audit_load_more`);
130
+ // «موارد بیشتر» was a noun phrase where the English is an action, and reached for a noun the rest
131
+ // of this bundle does not use.
132
+ 'inventory.load_more': 'بارگذاری بیشتر',
133
+ 'inventory.row_actions': 'اقدام‌های {name}',
134
+ 'inventory.archive_title': '{name} بایگانی شود؟',
135
+ 'inventory.archive_body':
136
+ 'از فهرست کنار می‌رود و کد خود را نگه می‌دارد. چیزی حذف نمی‌شود و هر زمان می‌توانید آن را بازگردانید.',
137
+ 'inventory.archived_toast': '{name} بایگانی شد',
138
+ 'inventory.restored_toast': '{name} بازگردانده شد',
139
+ 'inventory.created_toast': '{name} افزوده شد',
140
+ 'inventory.updated_toast': '{name} به‌روزرسانی شد',
141
+ 'inventory.load_error': 'اقلام بارگذاری نشد',
142
+ 'inventory.no_matches': 'چیزی پیدا نشد',
143
+ 'inventory.no_matches_desc': 'هیچ قلمی با این جستجو یا این فیلترها همخوانی ندارد.',
144
+ 'inventory.clear_filters': 'پاک کردن فیلترها',
145
+ 'inventory.settings_general': 'عمومی',
146
+ 'inventory.settings_general_desc': 'شیوهٔ شماره‌گذاری اموال در این فضای کاری.',
147
+ 'inventory.settings_numbering': 'کد اموال',
148
+ 'inventory.settings_numbering_desc':
149
+ 'شکل کدهایی که از این پس صادر می‌شود. کدهایی که روی برچسب‌ها هستند تغییر نمی‌کنند.',
150
+ 'inventory.settings_prefix': 'پیشوند',
151
+ 'inventory.settings_prefix_hint': 'حداکثر ۸ نویسه، یا خالی برای فقط عدد.',
152
+ 'inventory.settings_prefix_too_long': 'حداکثر ۸ نویسه',
153
+ 'inventory.settings_pad': 'تعداد رقم',
154
+ 'inventory.settings_pad_hint': 'عدد تا چند رقم با صفر پر شود.',
155
+ 'inventory.settings_pad_invalid': 'عددی درست از ۱ تا ۱۰ وارد کنید',
156
+ 'inventory.settings_preview': 'نمونهٔ کد',
157
+ 'inventory.settings_saved': 'تنظیمات کد اموال ذخیره شد',
158
+ 'inventory.settings_save_error': 'تنظیمات ذخیره نشد',
159
+ 'inventory.settings_error': 'تنظیمات بارگذاری نشد',
160
+ 'inventory.settings_readonly': 'این تنظیمات را می‌بینید اما نمی‌توانید تغییر دهید.',
161
+ 'inventory.settings_not_enabled': 'ماژول اموال در این فضای کاری فعال نیست',
162
+ 'inventory.widget_title': 'اقلام اخیر',
163
+ 'inventory.widget_desc': 'آخرین اقلامی که به این فضای کاری اضافه شده‌اند.',
164
+ }
165
+
166
+ export const ar: Record<string, Message> = {
167
+ 'inventory.nav': 'الأصول',
168
+ 'inventory.title': 'الأصول',
169
+ 'inventory.empty': 'لا أصول بعد',
170
+ 'inventory.empty_desc': 'أضف أول حاسوب أو هاتف أو مكتب — أي شيء تملكه الشركة.',
171
+ 'inventory.new': 'أصل جديد',
172
+ 'inventory.edit': 'تعديل الأصل',
173
+ 'inventory.code': 'رمز الأصل',
174
+ 'inventory.name': 'الاسم',
175
+ // A brand and model stay in Latin script: it is what is printed on the lid, and «ماك بوك برو ١٤»
176
+ // is not a string anybody would type into this field or read off a sticker.
177
+ 'inventory.name_placeholder': 'MacBook Pro 14"، كرسي مكتبي…',
178
+ 'inventory.description': 'الوصف',
179
+ 'inventory.status': 'الحالة',
180
+ 'inventory.serial_number': 'الرقم التسلسلي',
181
+ 'inventory.location': 'الموقع',
182
+ 'inventory.purchased_on': 'تاريخ الشراء',
183
+ 'inventory.purchased_from': 'تم الشراء من',
184
+ 'inventory.price': 'السعر',
185
+ 'inventory.warranty_until': 'الضمان حتى',
186
+ 'inventory.status_in_stock': 'في المخزن',
187
+ 'inventory.status_assigned': 'مُسلَّم',
188
+ 'inventory.status_reserved': 'محجوز',
189
+ 'inventory.status_under_repair': 'قيد الإصلاح',
190
+ 'inventory.status_lost': 'مفقود',
191
+ 'inventory.status_retired': 'مستبعد',
192
+ 'inventory.currency': 'العملة',
193
+ 'inventory.price_invalid': 'أدخل مبلغًا مثل {example}',
194
+ 'inventory.count': {
195
+ zero: 'لا أصول',
196
+ one: 'أصل واحد',
197
+ two: 'أصلان',
198
+ few: '{n} أصول',
199
+ many: '{n} أصلاً',
200
+ other: '{n} أصل',
201
+ },
202
+ 'inventory.count_showing': {
203
+ zero: 'لا أصول معروضة',
204
+ one: 'يُعرض أصل واحد',
205
+ two: 'يُعرض أصلان',
206
+ few: 'تُعرض {n} أصول',
207
+ many: 'يُعرض {n} أصلاً',
208
+ other: 'يُعرض {n} أصل',
209
+ },
210
+ 'inventory.search_placeholder': 'ابحث بالاسم أو الرمز أو الرقم التسلسلي…',
211
+ 'inventory.filter_all_statuses': 'جميع الحالات',
212
+ 'inventory.archived': 'مؤرشف',
213
+ 'inventory.restore': 'استعادة',
214
+ 'inventory.show_archived': 'إظهار المؤرشفة',
215
+ 'inventory.load_more': 'تحميل المزيد',
216
+ 'inventory.row_actions': 'إجراءات {name}',
217
+ 'inventory.archive_title': 'أرشفة {name}؟',
218
+ 'inventory.archive_body': 'يختفي من القائمة ويحتفظ برمزه. لا يُحذف شيء، ويمكنك استعادته في أي وقت.',
219
+ 'inventory.archived_toast': 'تمت أرشفة {name}',
220
+ 'inventory.restored_toast': 'تمت استعادة {name}',
221
+ 'inventory.created_toast': 'تمت إضافة {name}',
222
+ 'inventory.updated_toast': 'تم تحديث {name}',
223
+ 'inventory.load_error': 'تعذّر تحميل الأصول',
224
+ 'inventory.no_matches': 'لا نتائج',
225
+ 'inventory.no_matches_desc': 'لا يطابق أي أصل هذا البحث أو هذه المرشِّحات.',
226
+ 'inventory.clear_filters': 'مسح المرشِّحات',
227
+ 'inventory.settings_general': 'عام',
228
+ 'inventory.settings_general_desc': 'كيف ترقّم مساحة العمل هذه ما تملكه.',
229
+ 'inventory.settings_numbering': 'رموز الأصول',
230
+ 'inventory.settings_numbering_desc': 'شكل كل رمز يصدر من الآن. الرموز الموجودة على الملصقات تبقى كما هي.',
231
+ 'inventory.settings_prefix': 'البادئة',
232
+ 'inventory.settings_prefix_hint': 'ثمانية أحرف على الأكثر، أو اتركها فارغة للأرقام وحدها.',
233
+ 'inventory.settings_prefix_too_long': 'استخدم ثمانية أحرف أو أقل',
234
+ 'inventory.settings_pad': 'عدد الخانات',
235
+ 'inventory.settings_pad_hint': 'إلى كم خانة يُستكمل الرقم بالأصفار.',
236
+ 'inventory.settings_pad_invalid': 'أدخل عددًا صحيحًا من ١ إلى ١٠',
237
+ 'inventory.settings_preview': 'مثال على الرمز',
238
+ 'inventory.settings_saved': 'حُفظت إعدادات رموز الأصول',
239
+ 'inventory.settings_save_error': 'تعذّر حفظ الإعدادات',
240
+ 'inventory.settings_error': 'تعذّر تحميل الإعدادات',
241
+ 'inventory.settings_readonly': 'يمكنك قراءة هذه الإعدادات دون تغييرها.',
242
+ 'inventory.settings_not_enabled': 'وحدة الأصول غير مفعّلة في مساحة العمل هذه',
243
+ 'inventory.widget_title': 'أصول حديثة',
244
+ 'inventory.widget_desc': 'الأصول المضافة أخيرًا إلى مساحة العمل هذه.',
245
+ }
246
+
247
+ export const de: Record<string, Message> = {
248
+ 'inventory.nav': 'Inventar',
249
+ 'inventory.title': 'Gegenstände',
250
+ 'inventory.empty': 'Noch keine Gegenstände',
251
+ 'inventory.empty_desc':
252
+ 'Fügen Sie den ersten Laptop, das erste Telefon oder den ersten Schreibtisch hinzu — alles, was die Firma besitzt.',
253
+ 'inventory.new': 'Neuer Gegenstand',
254
+ 'inventory.edit': 'Gegenstand bearbeiten',
255
+ 'inventory.code': 'Inventarnummer',
256
+ 'inventory.name': 'Name',
257
+ 'inventory.name_placeholder': 'MacBook Pro 14", Bürostuhl…',
258
+ 'inventory.description': 'Beschreibung',
259
+ 'inventory.status': 'Status',
260
+ 'inventory.serial_number': 'Seriennummer',
261
+ 'inventory.location': 'Standort',
262
+ 'inventory.purchased_on': 'Kaufdatum',
263
+ 'inventory.purchased_from': 'Gekauft bei',
264
+ 'inventory.price': 'Preis',
265
+ 'inventory.warranty_until': 'Garantie bis',
266
+ 'inventory.status_in_stock': 'Auf Lager',
267
+ 'inventory.status_assigned': 'Zugewiesen',
268
+ 'inventory.status_reserved': 'Reserviert',
269
+ 'inventory.status_under_repair': 'In Reparatur',
270
+ 'inventory.status_lost': 'Verloren',
271
+ 'inventory.status_retired': 'Ausgemustert',
272
+ 'inventory.currency': 'Währung',
273
+ 'inventory.price_invalid': 'Betrag wie {example} eingeben',
274
+ 'inventory.count': { one: '{n} Gegenstand', other: '{n} Gegenstände' },
275
+ 'inventory.count_showing': { one: '{n} Gegenstand angezeigt', other: '{n} Gegenstände angezeigt' },
276
+ 'inventory.search_placeholder': 'Nach Name, Nummer oder Seriennummer suchen…',
277
+ // "Status", not "Zustände": the shell renders this noun as *Status* in all eight places it has
278
+ // one, and reserves *Zustand* for a module's health (`admin_modules_col_health`).
279
+ 'inventory.filter_all_statuses': 'Alle Status',
280
+ 'inventory.archived': 'Archiviert',
281
+ 'inventory.restore': 'Wiederherstellen',
282
+ 'inventory.show_archived': 'Archivierte anzeigen',
283
+ 'inventory.load_more': 'Mehr laden',
284
+ 'inventory.row_actions': 'Aktionen für {name}',
285
+ 'inventory.archive_title': '{name} archivieren?',
286
+ 'inventory.archive_body':
287
+ 'Der Gegenstand verschwindet aus der Liste und behält seine Nummer. Es wird nichts gelöscht, und Sie können ihn jederzeit wiederherstellen.',
288
+ 'inventory.archived_toast': '{name} archiviert',
289
+ 'inventory.restored_toast': '{name} wiederhergestellt',
290
+ 'inventory.created_toast': '{name} hinzugefügt',
291
+ 'inventory.updated_toast': '{name} aktualisiert',
292
+ 'inventory.load_error': 'Die Gegenstände konnten nicht geladen werden',
293
+ 'inventory.no_matches': 'Keine Treffer',
294
+ 'inventory.no_matches_desc': 'Kein Gegenstand passt zu dieser Suche oder diesen Filtern.',
295
+ 'inventory.clear_filters': 'Filter zurücksetzen',
296
+ 'inventory.settings_general': 'Allgemein',
297
+ 'inventory.settings_general_desc': 'Wie dieser Arbeitsbereich seinen Besitz nummeriert.',
298
+ 'inventory.settings_numbering': 'Inventarnummern',
299
+ 'inventory.settings_numbering_desc':
300
+ 'Die Form jeder ab jetzt vergebenen Nummer. Bereits aufgeklebte Nummern bleiben, wie sie sind.',
301
+ 'inventory.settings_prefix': 'Präfix',
302
+ 'inventory.settings_prefix_hint': 'Höchstens 8 Zeichen, oder leer für reine Ziffern.',
303
+ 'inventory.settings_prefix_too_long': 'Höchstens 8 Zeichen verwenden',
304
+ 'inventory.settings_pad': 'Stellen',
305
+ 'inventory.settings_pad_hint': 'Auf wie viele Stellen die Zahl mit Nullen aufgefüllt wird.',
306
+ 'inventory.settings_pad_invalid': 'Ganze Zahl von 1 bis 10 eingeben',
307
+ 'inventory.settings_preview': 'Beispielnummer',
308
+ 'inventory.settings_saved': 'Einstellungen für Inventarnummern gespeichert',
309
+ 'inventory.settings_save_error': 'Die Einstellungen konnten nicht gespeichert werden',
310
+ 'inventory.settings_error': 'Die Einstellungen konnten nicht geladen werden',
311
+ 'inventory.settings_readonly': 'Sie können diese Einstellungen lesen, aber nicht ändern.',
312
+ 'inventory.settings_not_enabled': 'Inventar ist in diesem Arbeitsbereich nicht aktiviert',
313
+ 'inventory.widget_title': 'Neue Gegenstände',
314
+ 'inventory.widget_desc': 'Die zuletzt hinzugefügten Gegenstände dieses Arbeitsbereichs.',
315
+ }
316
+
317
+ export const tr: Record<string, Message> = {
318
+ 'inventory.nav': 'Envanter',
319
+ 'inventory.title': 'Demirbaşlar',
320
+ 'inventory.empty': 'Henüz demirbaş yok',
321
+ 'inventory.empty_desc': 'İlk dizüstünü, telefonu ya da masayı ekleyin — şirkete ait her şeyi.',
322
+ 'inventory.new': 'Yeni demirbaş',
323
+ 'inventory.edit': 'Demirbaşı düzenle',
324
+ 'inventory.code': 'Demirbaş no',
325
+ 'inventory.name': 'Ad',
326
+ 'inventory.name_placeholder': 'MacBook Pro 14", ofis koltuğu…',
327
+ 'inventory.description': 'Açıklama',
328
+ 'inventory.status': 'Durum',
329
+ 'inventory.serial_number': 'Seri numarası',
330
+ 'inventory.location': 'Konum',
331
+ 'inventory.purchased_on': 'Alım tarihi',
332
+ 'inventory.purchased_from': 'Alındığı yer',
333
+ 'inventory.price': 'Fiyat',
334
+ 'inventory.warranty_until': 'Garanti bitişi',
335
+ 'inventory.status_in_stock': 'Depoda',
336
+ 'inventory.status_assigned': 'Zimmetli',
337
+ 'inventory.status_reserved': 'Ayrılmış',
338
+ 'inventory.status_under_repair': 'Onarımda',
339
+ 'inventory.status_lost': 'Kayıp',
340
+ // Adjectival, like every other badge in this set (Depoda, Zimmetli, Onarımda, Kayıp). "Hurdaya
341
+ // ayrıldı" is a finished sentence, and a table cell reporting a state does not narrate an event.
342
+ 'inventory.status_retired': 'Hurdaya ayrılmış',
343
+ 'inventory.currency': 'Para birimi',
344
+ 'inventory.price_invalid': '{example} gibi bir tutar girin',
345
+ 'inventory.count': { one: '{n} demirbaş', other: '{n} demirbaş' },
346
+ 'inventory.count_showing': { one: '{n} demirbaş gösteriliyor', other: '{n} demirbaş gösteriliyor' },
347
+ 'inventory.search_placeholder': 'Ada, numaraya veya seri numarasına göre ara…',
348
+ 'inventory.filter_all_statuses': 'Tüm durumlar',
349
+ 'inventory.archived': 'Arşivlenmiş',
350
+ // Not "Geri al": tr.json already spends that on *undo* and *revoke* (`undo`, `dash_undo`,
351
+ // `invite_revoke`), so the same two words in a row menu would read as "undo" beside "Arşivle".
352
+ 'inventory.restore': 'Geri yükle',
353
+ 'inventory.show_archived': 'Arşivlenmişleri göster',
354
+ // The shell's own catalogue says "Daha fazla yükle" for this English string (`audit_load_more`).
355
+ 'inventory.load_more': 'Daha fazla yükle',
356
+ 'inventory.row_actions': '{name} için işlemler',
357
+ 'inventory.archive_title': '{name} arşivlensin mi?',
358
+ 'inventory.archive_body':
359
+ 'Listeden çıkar ve numarasını korur. Hiçbir şey silinmez, istediğiniz zaman geri yükleyebilirsiniz.',
360
+ 'inventory.archived_toast': '{name} arşivlendi',
361
+ 'inventory.restored_toast': '{name} geri yüklendi',
362
+ 'inventory.created_toast': '{name} eklendi',
363
+ 'inventory.updated_toast': '{name} güncellendi',
364
+ 'inventory.load_error': 'Demirbaşlar yüklenemedi',
365
+ 'inventory.no_matches': 'Eşleşen bir şey yok',
366
+ 'inventory.no_matches_desc': 'Bu aramaya veya bu filtrelere uyan bir demirbaş yok.',
367
+ 'inventory.clear_filters': 'Filtreleri temizle',
368
+ 'inventory.settings_general': 'Genel',
369
+ 'inventory.settings_general_desc': 'Bu çalışma alanının sahip olduklarını nasıl numaralandırdığı.',
370
+ 'inventory.settings_numbering': 'Demirbaş numaraları',
371
+ 'inventory.settings_numbering_desc':
372
+ 'Bundan sonra verilecek her numaranın biçimi. Etikete basılmış numaralar olduğu gibi kalır.',
373
+ 'inventory.settings_prefix': 'Ön ek',
374
+ 'inventory.settings_prefix_hint': 'En çok 8 karakter, ya da yalnızca rakam için boş bırakın.',
375
+ 'inventory.settings_prefix_too_long': 'En çok 8 karakter kullanın',
376
+ 'inventory.settings_pad': 'Basamak',
377
+ 'inventory.settings_pad_hint': 'Sayının kaç basamağa kadar sıfırla tamamlanacağı.',
378
+ 'inventory.settings_pad_invalid': '1 ile 10 arasında bir tam sayı girin',
379
+ 'inventory.settings_preview': 'Örnek numara',
380
+ 'inventory.settings_saved': 'Demirbaş numarası ayarları kaydedildi',
381
+ 'inventory.settings_save_error': 'Ayarlar kaydedilemedi',
382
+ 'inventory.settings_error': 'Ayarlar yüklenemedi',
383
+ 'inventory.settings_readonly': 'Bu ayarları görebilir ama değiştiremezsiniz.',
384
+ 'inventory.settings_not_enabled': 'Envanter bu çalışma alanında etkin değil',
385
+ 'inventory.widget_title': 'Son eklenenler',
386
+ 'inventory.widget_desc': 'Bu çalışma alanına en son eklenen demirbaşlar.',
387
+ }
388
+
389
+ /**
390
+ * Add a locale by adding a bundle here. Bundles are thunks so a locale is fetched only when it is
391
+ * the one in use; English is the fallback and is therefore always loaded.
392
+ */
393
+ export const inventoryMessageBundles = {
394
+ ar: async () => ar,
395
+ de: async () => de,
396
+ en: async () => en,
397
+ fa: async () => fa,
398
+ tr: async () => tr,
399
+ }
@@ -0,0 +1,161 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createMockInventoryApi } from './mock.js'
3
+
4
+ /**
5
+ * The demo, held to the server's behaviour.
6
+ *
7
+ * A mock is the environment the product is *shown* in, so a mock that disagrees with the server is
8
+ * a demo that argues with its own controls — and every disagreement here was a real one: seed order
9
+ * where the server sorts newest first, `INV-0013` for the seventh asset, `sort`/`categoryId`/
10
+ * `custodianUserId` ignored, and an unrecognised cursor quietly serving page one for ever.
11
+ *
12
+ * The cursor assertions are the load-bearing ones. `src/server/services/assets.ts` answers a marker
13
+ * it did not issue with `BAD_REQUEST`; a mock that answers it with page one turns a bug into an
14
+ * endless "Load more" that nobody meets until production.
15
+ */
16
+ const WS = '01920000-0000-7000-8000-0000000000ff'
17
+
18
+ const codes = (items: { code: string }[]) => items.map((a) => a.code)
19
+
20
+ describe('mock assets.list', () => {
21
+ it('opens on what was added last, like the server’s default sort', async () => {
22
+ const api = createMockInventoryApi()
23
+ const { items } = await api.assets.list({ workspaceId: WS })
24
+ // INV-0006 is archived and therefore absent; the rest are newest first.
25
+ expect(codes(items)).toEqual(['INV-0005', 'INV-0004', 'INV-0003', 'INV-0002', 'INV-0001'])
26
+ })
27
+
28
+ it('honours sort=code and sort=name, which it used to ignore', async () => {
29
+ const api = createMockInventoryApi()
30
+ expect(codes((await api.assets.list({ workspaceId: WS, sort: 'code' })).items)).toEqual([
31
+ 'INV-0001',
32
+ 'INV-0002',
33
+ 'INV-0003',
34
+ 'INV-0004',
35
+ 'INV-0005',
36
+ ])
37
+ const byName = (await api.assets.list({ workspaceId: WS, sort: 'name' })).items
38
+ expect(byName.map((a) => a.name)).toEqual([...byName.map((a) => a.name)].sort())
39
+ })
40
+
41
+ it('honours the archived and status filters', async () => {
42
+ const api = createMockInventoryApi()
43
+ expect(codes((await api.assets.list({ workspaceId: WS, archived: true })).items)).toContain('INV-0006')
44
+ const assigned = await api.assets.list({ workspaceId: WS, status: 'assigned' })
45
+ expect(assigned.items.every((a) => a.status === 'assigned')).toBe(true)
46
+ expect(assigned.items).toHaveLength(2)
47
+ })
48
+
49
+ it('honours categoryId and custodianUserId, which it used to ignore', async () => {
50
+ const api = createMockInventoryApi()
51
+ const none = await api.assets.list({
52
+ workspaceId: WS,
53
+ categoryId: '01920000-0000-7000-8000-00000000aaaa',
54
+ })
55
+ expect(none.items).toEqual([])
56
+ const nobody = await api.assets.list({
57
+ workspaceId: WS,
58
+ custodianUserId: '01920000-0000-7000-8000-00000000bbbb',
59
+ })
60
+ expect(nobody.items).toEqual([])
61
+ })
62
+
63
+ it('pages through every row exactly once and then stops', async () => {
64
+ const api = createMockInventoryApi()
65
+ const seen: string[] = []
66
+ let cursor: string | undefined
67
+ for (let guard = 0; guard < 20; guard++) {
68
+ const page = await api.assets.list({ workspaceId: WS, limit: 2, ...(cursor ? { cursor } : {}) })
69
+ seen.push(...codes(page.items))
70
+ if (!page.nextCursor) break
71
+ cursor = page.nextCursor
72
+ }
73
+ expect(seen).toEqual(['INV-0005', 'INV-0004', 'INV-0003', 'INV-0002', 'INV-0001'])
74
+ expect(new Set(seen).size).toBe(seen.length)
75
+ })
76
+
77
+ it('reports no next page when the last page is exactly full', async () => {
78
+ const api = createMockInventoryApi()
79
+ const page = await api.assets.list({ workspaceId: WS, limit: 5 })
80
+ expect(page.items).toHaveLength(5)
81
+ expect(page.nextCursor).toBe(null)
82
+ })
83
+
84
+ it('refuses a page marker it did not issue, rather than serving page one for ever', async () => {
85
+ const api = createMockInventoryApi()
86
+ await expect(api.assets.list({ workspaceId: WS, cursor: 'not-a-cursor' })).rejects.toMatchObject({
87
+ code: 'BAD_REQUEST',
88
+ })
89
+ await expect(
90
+ api.assets.list({ workspaceId: WS, cursor: btoa(JSON.stringify({ i: 'nope', s: 'recent' })) }),
91
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' })
92
+ })
93
+
94
+ it('refuses a marker issued under a different sort', async () => {
95
+ const api = createMockInventoryApi()
96
+ const first = await api.assets.list({ workspaceId: WS, limit: 2, sort: 'recent' })
97
+ expect(first.nextCursor).toBeTruthy()
98
+ await expect(
99
+ api.assets.list({ workspaceId: WS, limit: 2, sort: 'code', cursor: first.nextCursor as string }),
100
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' })
101
+ })
102
+
103
+ it('ends the list when the bookmarked row is gone, rather than failing', async () => {
104
+ const api = createMockInventoryApi()
105
+ const first = await api.assets.list({ workspaceId: WS, limit: 2 })
106
+ const bookmarked = first.items.at(-1)
107
+ expect(bookmarked).toBeTruthy()
108
+ // Archiving the bookmarked row still leaves it findable by id, so the page continues; a row the
109
+ // mock genuinely cannot find is the "ends quietly" branch, and an unknown *id* is refused above.
110
+ await api.assets.archive({ assetId: (bookmarked as { id: string }).id })
111
+ const next = await api.assets.list({ workspaceId: WS, limit: 2, cursor: first.nextCursor as string })
112
+ expect(next.items.every((a) => !codes(first.items).includes(a.code))).toBe(true)
113
+ })
114
+ })
115
+
116
+ describe('mock assets.create', () => {
117
+ it('continues the tag sequence from the seeds', async () => {
118
+ const api = createMockInventoryApi()
119
+ // It issued INV-0013 here: one counter was numbering both the ids and the tags.
120
+ expect((await api.assets.create({ workspaceId: WS, name: 'Standing desk' })).code).toBe('INV-0007')
121
+ expect((await api.assets.create({ workspaceId: WS, name: 'Label printer' })).code).toBe('INV-0008')
122
+ })
123
+
124
+ it('puts what was just created at the top of the default list', async () => {
125
+ const api = createMockInventoryApi()
126
+ await api.assets.create({ workspaceId: WS, name: 'Standing desk' })
127
+ const { items } = await api.assets.list({ workspaceId: WS })
128
+ expect(items[0]?.name).toBe('Standing desk')
129
+ })
130
+ })
131
+
132
+ describe('mock assets.update and archive', () => {
133
+ it('patches only what it was given', async () => {
134
+ const api = createMockInventoryApi()
135
+ const [first] = (await api.assets.list({ workspaceId: WS })).items
136
+ const updated = await api.assets.update({
137
+ workspaceId: WS,
138
+ assetId: (first as { id: string }).id,
139
+ name: 'Renamed',
140
+ })
141
+ expect(updated.name).toBe('Renamed')
142
+ expect(updated.code).toBe(first?.code)
143
+ // Routing, not a field of the asset: a patch must not be able to move a row between workspaces.
144
+ expect(updated.workspaceId).toBe(first?.workspaceId)
145
+ })
146
+
147
+ it('archives and restores the same row', async () => {
148
+ const api = createMockInventoryApi()
149
+ const [first] = (await api.assets.list({ workspaceId: WS })).items
150
+ const id = (first as { id: string }).id
151
+ expect((await api.assets.archive({ assetId: id })).archivedAt).toBeTruthy()
152
+ expect(codes((await api.assets.list({ workspaceId: WS })).items)).not.toContain(first?.code)
153
+ expect((await api.assets.archive({ assetId: id, archived: false })).archivedAt).toBe(null)
154
+ expect(codes((await api.assets.list({ workspaceId: WS })).items)).toContain(first?.code)
155
+ })
156
+
157
+ it('reports a missing asset as NOT_FOUND', async () => {
158
+ const api = createMockInventoryApi()
159
+ await expect(api.assets.get({ assetId: 'nope' })).rejects.toMatchObject({ code: 'NOT_FOUND' })
160
+ })
161
+ })