@7365admin1/layer-common 4.62.0 → 4.63.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.63.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cd0a5aa: Worker nouns follow the account type: one term map (`utils/worker-terms.ts`, `useWorkerTerm()`), keyed by the app's `appSlug`. Cleaning says Cleaners, security Security officers, pest control Pest technicians, M&E Technicians, landscape Gardeners, pool Pool attendants, property management Staff / Service staff, anything else Staff. Applied to Inventory (tab, report, usage column, export file name and sheet headers via `appSlug` on the export), Attendance, the schedule checklists, the security dashboard and NFC patrol report. Display only - field names and API keys are unchanged. `scripts/sync-worker-terms.mjs` copies the map into the mobile apps and API-core.
8
+
3
9
  ## 4.62.0
4
10
 
5
11
  ### Minor Changes
@@ -1679,11 +1685,11 @@
1679
1685
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1680
1686
  own rule from two endpoints the console already calls, unprojected:
1681
1687
 
1682
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1683
- GET /api/roles/id/:role that membership's role document
1688
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1689
+ GET /api/roles/id/:role that membership's role document
1684
1690
 
1685
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1686
- staff = member.type === "admin" && role.type === "admin"
1691
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1692
+ staff = member.type === "admin" && role.type === "admin"
1687
1693
 
1688
1694
  `role.default` is the marker because it is the only property of a platform
1689
1695
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -2,7 +2,7 @@
2
2
  <v-row no-gutters align="center" justify="center">
3
3
  <v-col cols="12" lg="12">
4
4
  <TableMain
5
- :title="'Cleaner Checklist'"
5
+ :title="checklistTitle"
6
6
  :headers="headers"
7
7
  :items="items"
8
8
  :loading="loading"
@@ -98,6 +98,10 @@
98
98
 
99
99
  <script setup lang="ts">
100
100
  import useCleaningSchedules from "../composables/useCleaningSchedules";
101
+ import useWorkerTerm from "../composables/useWorkerTerm";
102
+ import { workerTitle } from "../utils/worker-terms";
103
+ // "Cleaner Checklist" in Hygiene, "Gardener Checklist" in Landscape, ...
104
+ const checklistTitle = workerTitle(`${useWorkerTerm().one} checklist`);
101
105
 
102
106
  const props = defineProps({
103
107
  orgId: { type: String, required: true, default: "" },
@@ -62,6 +62,7 @@
62
62
 
63
63
  <script setup lang="ts">
64
64
  import useAttendance from "../composables/useAttendance";
65
+ import useWorkerTerm from "../composables/useWorkerTerm";
65
66
  import useUtils from "../composables/useUtils";
66
67
 
67
68
  const props = defineProps({
@@ -86,7 +87,7 @@ const searchInput = ref("");
86
87
  const items = ref<Array<Record<string, any>>>([]);
87
88
 
88
89
  const headers = [
89
- { title: "Cleaner", value: "userName" },
90
+ { title: useWorkerTerm().one, value: "userName" },
90
91
  { title: "Check In", value: "checkInTimestamp" },
91
92
  { title: "Check Out", value: "checkOutTimestamp" },
92
93
  { title: "Total Hours", value: "totalHours" },
@@ -3,7 +3,7 @@
3
3
  <v-col cols="12" lg="12">
4
4
  <TableMain
5
5
  v-if="canViewSchedules"
6
- :title="'Cleaner Checklist'"
6
+ :title="checklistTitle"
7
7
  :items="items"
8
8
  :headers="headers"
9
9
  :loading="loading"
@@ -11,7 +11,7 @@
11
11
  v-model:page="page"
12
12
  :pages="pages"
13
13
  :pageRange="pageRange"
14
- :no-data-text="'No Cleaner Checklist found'"
14
+ :no-data-text="`No ${checklistTitle} found`"
15
15
  @refresh="getCleanerChecklistRefresh"
16
16
  @row-click="onRowClick"
17
17
  >
@@ -114,6 +114,10 @@
114
114
 
115
115
  <script lang="ts" setup>
116
116
  import useCleaningSchedules from "../composables/useCleaningSchedules";
117
+ import useWorkerTerm from "../composables/useWorkerTerm";
118
+ import { workerTitle } from "../utils/worker-terms";
119
+ // "Cleaner Checklist" in Hygiene, "Gardener Checklist" in Landscape, ...
120
+ const checklistTitle = workerTitle(`${useWorkerTerm().one} checklist`);
117
121
  import useUtils from "../composables/useUtils";
118
122
 
119
123
  const props = defineProps({
@@ -996,7 +996,7 @@
996
996
  <strong>{{
997
997
  item.userName || item.personName || "Unassigned"
998
998
  }}</strong>
999
- <p>{{ item.role || item.designation || "Officer" }}</p>
999
+ <p>{{ item.role || item.designation || worker.one }}</p>
1000
1000
  </div>
1001
1001
  <StatusChip :status="item.status || 'On duty'" pill />
1002
1002
  </div>
@@ -1233,6 +1233,7 @@ import type { CookieOptions } from "#app";
1233
1233
  import { buildWorkOrderStatus } from "../utils/dashboard";
1234
1234
  import { permissionsDenied } from "../utils/module-routes";
1235
1235
  import { grantsPermission } from "../utils/permission-spellings";
1236
+ import { workerTerm, workerTitle } from "../utils/worker-terms";
1236
1237
  import {
1237
1238
  hiddenDashboardWidgets,
1238
1239
  saveHiddenDashboardWidgets,
@@ -1369,6 +1370,8 @@ const isPropertyMode = computed(
1369
1370
  () => props.serviceType === "property_management_agency"
1370
1371
  );
1371
1372
  const isSecurityMode = computed(() => props.serviceType === "security_agency");
1373
+ // "Security officer" on the security dashboard; each service type's own noun elsewhere.
1374
+ const worker = computed(() => workerTerm(props.serviceType));
1372
1375
  const isHygieneMode = computed(
1373
1376
  () => !isPropertyMode.value && !isSecurityMode.value
1374
1377
  );
@@ -2963,7 +2966,7 @@ const printTableSections = computed<PrintTableSection[]>(() => {
2963
2966
  columns: [
2964
2967
  { key: "title", label: "Patrol Route", width: "30%", strong: true },
2965
2968
  { key: "subtitle", label: "Location", width: "30%" },
2966
- { key: "person", label: "Assigned Officer", width: "25%" },
2969
+ { key: "person", label: workerTitle(`Assigned ${worker.value.one}`), width: "25%" },
2967
2970
  { key: "status", label: "Status", width: "15%", status: true },
2968
2971
  ],
2969
2972
  rows: activePatrolItems.value.map((item, index) => ({
@@ -2978,7 +2981,7 @@ const printTableSections = computed<PrintTableSection[]>(() => {
2978
2981
  key: "staffStatus",
2979
2982
  title: "Staff Status",
2980
2983
  columns: [
2981
- { key: "name", label: "Officer Name", width: "40%", strong: true },
2984
+ { key: "name", label: workerTitle(`${worker.value.one} name`), width: "40%", strong: true },
2982
2985
  { key: "role", label: "Role / Designation", width: "40%" },
2983
2986
  {
2984
2987
  key: "status",
@@ -2991,7 +2994,7 @@ const printTableSections = computed<PrintTableSection[]>(() => {
2991
2994
  rows: securityStaffItems.value.map((item, index) => ({
2992
2995
  id: item.id || item._id || `security-staff-${index}`,
2993
2996
  name: item.userName || item.personName || "Unassigned",
2994
- role: item.role || item.designation || "Officer",
2997
+ role: item.role || item.designation || worker.value.one,
2995
2998
  status: item.status || "On duty",
2996
2999
  })),
2997
3000
  }
@@ -3927,7 +3930,7 @@ async function exportDashboard(format: "PDF" | "Word") {
3927
3930
  if (isSecurityMode.value) {
3928
3931
  wordHtml += `<div class="section-title">Active Patrol</div>`;
3929
3932
  wordHtml += `<table class="data-table">`;
3930
- wordHtml += `<thead><tr><th>Patrol Route</th><th>Location</th><th>Assigned Officer</th><th>Status</th></tr></thead><tbody>`;
3933
+ wordHtml += `<thead><tr><th>Patrol Route</th><th>Location</th><th>${workerTitle(`Assigned ${worker.value.one}`)}</th><th>Status</th></tr></thead><tbody>`;
3931
3934
  if (activePatrolItems.value.length > 0) {
3932
3935
  activePatrolItems.value.forEach((item) => {
3933
3936
  wordHtml += `<tr>
@@ -3946,14 +3949,14 @@ async function exportDashboard(format: "PDF" | "Word") {
3946
3949
 
3947
3950
  wordHtml += `<div class="section-title">Staff Status</div>`;
3948
3951
  wordHtml += `<table class="data-table">`;
3949
- wordHtml += `<thead><tr><th>Officer Name</th><th>Role / Designation</th><th>Status</th></tr></thead><tbody>`;
3952
+ wordHtml += `<thead><tr><th>${workerTitle(`${worker.value.one} name`)}</th><th>Role / Designation</th><th>Status</th></tr></thead><tbody>`;
3950
3953
  if (securityStaffItems.value.length > 0) {
3951
3954
  securityStaffItems.value.forEach((item) => {
3952
3955
  wordHtml += `<tr>
3953
3956
  <td><strong>${
3954
3957
  item.userName || item.personName || "Unassigned"
3955
3958
  }</strong></td>
3956
- <td>${item.role || item.designation || "Officer"}</td>
3959
+ <td>${item.role || item.designation || worker.value.one}</td>
3957
3960
  <td><span class="status-badge badge-${getStatusTone(
3958
3961
  item.status
3959
3962
  )}">${item.status || "On duty"}</span></td>
@@ -112,6 +112,7 @@
112
112
 
113
113
  <script setup lang="ts">
114
114
  import useInventory from "../composables/useInventory";
115
+ import useWorkerTerm from "../composables/useWorkerTerm";
115
116
  import useSite from "../composables/useSite";
116
117
  import useUtils from "../composables/useUtils";
117
118
  import { inventoryStoreLabel } from "../utils/inventory-store-label";
@@ -160,6 +161,9 @@ const subtitle = computed(() => {
160
161
  return inventoryStoreLabel(store.value, stores.value);
161
162
  });
162
163
 
164
+ // Who holds this store's stock: the property manager's store lends to service providers' workers.
165
+ const worker = useWorkerTerm({ providers: true });
166
+
163
167
  const tabs = computed(() => {
164
168
  const a = store.value?.access;
165
169
  if (!a) return [];
@@ -167,7 +171,7 @@ const tabs = computed(() => {
167
171
  { value: "stock", label: "Stock" },
168
172
  ...(a.canRequest && !a.canManage ? [{ value: "mine", label: "My Stock" }] : []),
169
173
  { value: "requests", label: a.canApprove ? "Requests" : "My Requests", badge: a.canApprove ? pendingCount.value : 0 },
170
- ...(a.canApprove ? [{ value: "technicians", label: "Technicians" }] : []),
174
+ ...(a.canApprove ? [{ value: "technicians", label: worker.many }] : []),
171
175
  ...(isStaff.value ? [{ value: "reports", label: "Reports" }] : []),
172
176
  ...(a.canManage ? [{ value: "settings", label: "Settings" }] : []),
173
177
  ];
@@ -4,7 +4,7 @@
4
4
  <v-btn-toggle v-model="report" mandatory density="comfortable" variant="outlined" divided>
5
5
  <v-btn value="movement" class="text-none">Stock Movement</v-btn>
6
6
  <v-btn value="usage" class="text-none">Usage</v-btn>
7
- <v-btn value="technicians" class="text-none">Technicians</v-btn>
7
+ <v-btn value="technicians" class="text-none">{{ worker.many }}</v-btn>
8
8
  <v-btn value="purchases" class="text-none">Purchases</v-btn>
9
9
  </v-btn-toggle>
10
10
  <v-select
@@ -133,6 +133,7 @@
133
133
 
134
134
  <script setup lang="ts">
135
135
  import useInventory, { saveBlob } from "../composables/useInventory";
136
+ import useWorkerTerm from "../composables/useWorkerTerm";
136
137
  import useUtils from "../composables/useUtils";
137
138
  import useFile from "../composables/useFile";
138
139
 
@@ -150,6 +151,9 @@ const usagePhotos = (row: any) => [
150
151
  ];
151
152
  const notify = inject<(text: string, color?: string) => void>("inventoryNotify", () => {});
152
153
 
154
+ const worker = useWorkerTerm({ providers: true });
155
+ const appSlug = useRuntimeConfig().public?.appSlug as string | undefined;
156
+
153
157
  const now = new Date();
154
158
  const month = ref(`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`);
155
159
  const report = ref<"movement" | "usage" | "technicians" | "purchases">("movement");
@@ -204,7 +208,7 @@ const headers: Record<string, any[]> = {
204
208
  { title: "Qty", key: "qty" },
205
209
  { title: "Unit price", key: "unitCost" },
206
210
  { title: "Total", key: "totalCost" },
207
- { title: "Technician", key: "technician" },
211
+ { title: worker.one, key: "technician" },
208
212
  { title: "Photos", key: "photos", sortable: false },
209
213
  { title: "Reference", key: "reference" },
210
214
  { title: "Reported by", key: "reportedBy" },
@@ -283,8 +287,8 @@ async function setChecked(checked: boolean) {
283
287
  async function exportExcel() {
284
288
  exporting.value = true;
285
289
  try {
286
- const blob = await api.exportReport(props.store._id, report.value, period());
287
- saveBlob(blob, `${props.store.name} - ${report.value} - ${month.value}.xlsx`);
290
+ const blob = await api.exportReport(props.store._id, report.value, { ...period(), appSlug });
291
+ saveBlob(blob, `${props.store.name} - ${report.value === "technicians" ? worker.many : report.value} - ${month.value}.xlsx`);
288
292
  } catch {
289
293
  notify("Could not export the report.", "error");
290
294
  } finally {
@@ -460,6 +460,8 @@
460
460
  import { readListPage } from "../utils/list-page";
461
461
  import { isPhotoRefusal, isSetFinished } from "../utils/checklist-set";
462
462
  import useCleaningSchedules from "../composables/useCleaningSchedules";
463
+ import useWorkerTerm from "../composables/useWorkerTerm";
464
+ import { workerTitle } from "../utils/worker-terms";
463
465
  import useCustomerSite from "../composables/useCustomerSite";
464
466
  import useFile from "../composables/useFile";
465
467
  import useUtils from "../composables/useUtils";
@@ -480,14 +482,15 @@ const { getBySiteAsServiceProvider } = useCustomerSite();
480
482
  const { back } = useUtils();
481
483
 
482
484
  const route = useRoute();
485
+ const workerChecklist = workerTitle(`${useWorkerTerm().one} checklist`);
483
486
  const scheduleTitle = computed(() => {
484
487
  const path = route.path;
485
488
  if (path.includes("cleaning-schedule")) return "Cleaning Checklist";
486
- if (path.includes("technician-schedule")) return "Technician Checklist";
489
+ if (path.includes("technician-schedule")) return workerChecklist;
487
490
  if (path.includes("landscape-schedule")) return "Landscape Checklist";
488
491
  if (path.includes("pool-schedule")) return "Pool Checklist";
489
492
  if (path.includes("pest-schedule")) return "Pest Checklist";
490
- return "Cleaner Checklist";
493
+ return workerChecklist;
491
494
  });
492
495
 
493
496
  const canAddRemarks = computed(() => props.canAddRemarks);
@@ -45,7 +45,7 @@
45
45
  <v-img
46
46
  v-if="activity.guardSignatureFileId"
47
47
  :src="getFileUrl(activity.guardSignatureFileId)"
48
- :alt="`${activity.guardName || 'Guard'} signature`"
48
+ :alt="`${activity.guardName || worker.one} signature`"
49
49
  class="guard-signature"
50
50
  contain
51
51
  />
@@ -78,11 +78,13 @@
78
78
  </template>
79
79
 
80
80
  <script setup lang="ts">
81
+ import useWorkerTerm from "../../../composables/useWorkerTerm";
81
82
  import type {
82
83
  NFCPatrolActivity,
83
84
  NFCPatrolActivityStatus,
84
85
  } from "../../../types/nfc-patrol-report";
85
86
 
87
+ const worker = useWorkerTerm();
86
88
  defineProps<{
87
89
  activities: NFCPatrolActivity[];
88
90
  }>();
@@ -44,7 +44,7 @@
44
44
  <v-img
45
45
  v-if="row.guardSignatureFileId"
46
46
  :src="getFileUrl(row.guardSignatureFileId)"
47
- :alt="`${row.guardName || 'Guard'} signature`"
47
+ :alt="`${row.guardName || worker.one} signature`"
48
48
  class="guard-signature"
49
49
  contain
50
50
  />
@@ -65,11 +65,13 @@
65
65
  </template>
66
66
 
67
67
  <script setup lang="ts">
68
+ import useWorkerTerm from "../../../composables/useWorkerTerm";
68
69
  import type {
69
70
  NFCPatrolReportSummaryRow,
70
71
  NFCPatrolReportSummaryStatus,
71
72
  } from "../../../types/nfc-patrol-report";
72
73
 
74
+ const worker = useWorkerTerm();
73
75
  defineProps<{
74
76
  title: string;
75
77
  rows: NFCPatrolReportSummaryRow[];
@@ -2,7 +2,7 @@
2
2
  <v-row no-gutters align="center" justify="center">
3
3
  <v-col cols="12" lg="12">
4
4
  <TableMain
5
- :title="'Cleaner Checklist'"
5
+ :title="checklistTitle"
6
6
  :headers="headers"
7
7
  :items="items"
8
8
  :loading="loading"
@@ -160,6 +160,10 @@
160
160
 
161
161
  <script setup lang="ts">
162
162
  import useCleaningSchedules from "../composables/useCleaningSchedules";
163
+ import useWorkerTerm from "../composables/useWorkerTerm";
164
+ import { workerTitle } from "../utils/worker-terms";
165
+ // "Cleaner Checklist" in Hygiene, "Gardener Checklist" in Landscape, ...
166
+ const checklistTitle = workerTitle(`${useWorkerTerm().one} checklist`);
163
167
  import useUtils from "../composables/useUtils";
164
168
 
165
169
  const props = defineProps({
@@ -57,7 +57,7 @@ export default function useInventory() {
57
57
 
58
58
  getReport: (store: string, report: "movement" | "usage" | "technicians" | "purchases", query: TInventoryPeriod & { unchecked?: boolean }) =>
59
59
  api<{ rows: any[]; totalQty?: number; totalCost?: number }>(`/stores/${store}/reports/${report}`, { query }),
60
- exportReport: (store: string, report: string, query: TInventoryPeriod) =>
60
+ exportReport: (store: string, report: string, query: TInventoryPeriod & { appSlug?: string }) =>
61
61
  api<Blob>(`/stores/${store}/reports/${report}/export`, { query, responseType: "blob" }),
62
62
  checkUsage: (store: string, ids: string[], checked: boolean) =>
63
63
  api<{ updated: number }>(`/stores/${store}/usage/check`, { method: "POST", body: { ids, checked } }),
@@ -0,0 +1,10 @@
1
+ import { workerTerm } from "../utils/worker-terms";
2
+
3
+ /**
4
+ * The worker noun for the app this page runs in ("Cleaner" in Hygiene,
5
+ * "Security officer" in Security, ...). See utils/worker-terms.ts.
6
+ * Apps without `runtimeConfig.public.appSlug` get the neutral "Staff".
7
+ */
8
+ export default function useWorkerTerm(opts?: { providers?: boolean }) {
9
+ return workerTerm(useRuntimeConfig().public?.appSlug as string | undefined, opts);
10
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "4.62.0",
5
+ "version": "4.63.0",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
@@ -0,0 +1,80 @@
1
+ /**
2
+ * What each kind of account calls the people who do its work.
3
+ *
4
+ * A cleaning company's workers are cleaners, not technicians. Every screen,
5
+ * export and mobile app takes the noun from here, keyed by the app it runs in
6
+ * (`appSlug`, e.g. "iservice365-web-app-hygiene" or
7
+ * "iservice365-hygiene-mobile-app"); a service-type code ("hygiene",
8
+ * "security_agency", ...) resolves the same way. Only the words change -
9
+ * field names and API keys (`technician`, `usedByTechnicians`,
10
+ * `/reports/technicians`) stay as they are.
11
+ *
12
+ * Copied into the React Native apps and API-core by
13
+ * `scripts/sync-worker-terms.mjs`, so it must stay free of imports.
14
+ */
15
+
16
+ export type TWorkerTerm = {
17
+ /** Singular, sentence case: "Cleaner", "Security officer". */
18
+ one: string;
19
+ /** Plural, sentence case: "Cleaners", "Staff". */
20
+ many: string;
21
+ };
22
+
23
+ export type TWorkerKind =
24
+ | "hygiene"
25
+ | "security"
26
+ | "pest"
27
+ | "mechanical"
28
+ | "landscape"
29
+ | "pool"
30
+ | "property"
31
+ | "service"
32
+ | "staff";
33
+
34
+ export const WORKER_TERMS: Record<TWorkerKind, TWorkerTerm> = {
35
+ hygiene: { one: "Cleaner", many: "Cleaners" },
36
+ security: { one: "Security officer", many: "Security officers" },
37
+ pest: { one: "Pest technician", many: "Pest technicians" },
38
+ mechanical: { one: "Technician", many: "Technicians" },
39
+ landscape: { one: "Gardener", many: "Gardeners" },
40
+ pool: { one: "Pool attendant", many: "Pool attendants" },
41
+ // The property manager's own team.
42
+ property: { one: "Staff member", many: "Staff" },
43
+ // Other companies' workers, as a property manager sees them.
44
+ service: { one: "Service staff member", many: "Service staff" },
45
+ // Anything else (organisation console, Super Admin, unknown app).
46
+ staff: { one: "Staff member", many: "Staff" },
47
+ };
48
+
49
+ // ponytail: keyword match covers every web slug, mobile slug and service-type code in use; add a row when a new app kind appears.
50
+ const KINDS: [RegExp, TWorkerKind][] = [
51
+ [/hygiene|clean/, "hygiene"],
52
+ [/secur/, "security"],
53
+ [/pest/, "pest"],
54
+ [/mechanical|m-and-e/, "mechanical"],
55
+ [/landscap/, "landscape"],
56
+ [/pool/, "pool"],
57
+ [/property|-ma-/, "property"],
58
+ ];
59
+
60
+ export function workerKind(appSlug?: string | null): TWorkerKind {
61
+ const slug = String(appSlug ?? "").toLowerCase();
62
+ for (const [pattern, kind] of KINDS) if (pattern.test(slug)) return kind;
63
+ return "staff";
64
+ }
65
+
66
+ /**
67
+ * The noun for this app. `providers: true` is for lists of people from the
68
+ * service providers a property manager supplies (e.g. who holds stock from
69
+ * the property store): the property manager's app says "Service staff";
70
+ * every other app keeps its own noun.
71
+ */
72
+ export function workerTerm(appSlug?: string | null, opts?: { providers?: boolean }): TWorkerTerm {
73
+ const kind = workerKind(appSlug);
74
+ return WORKER_TERMS[opts?.providers && kind === "property" ? "service" : kind];
75
+ }
76
+
77
+ /** Title case for headings and column titles: "Security officer name" -> "Security Officer Name". */
78
+ export function workerTitle(text: string): string {
79
+ return String(text).replace(/(^|\s)(\S)/g, (_m, space: string, first: string) => space + first.toUpperCase());
80
+ }