create-nexwire 4.2.4 → 4.3.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 (113) hide show
  1. package/README.md +20 -8
  2. package/express/.env +1 -1
  3. package/express/.vscode/extensions.json +6 -0
  4. package/express/.vscode/settings.json +37 -26
  5. package/express/biome.json +99 -66
  6. package/express/package.json +31 -29
  7. package/express/src/config/database.ts +48 -29
  8. package/express/src/config/index.ts +83 -83
  9. package/express/src/config/openapi.ts +6 -24
  10. package/express/src/config/queue.ts +30 -30
  11. package/express/src/config/validate.ts +50 -40
  12. package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
  13. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  14. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  15. package/express/src/database/schema.ts +2 -2
  16. package/express/src/framework/cache/cache.ts +40 -8
  17. package/express/src/framework/circuit-breaker/cb.ts +88 -60
  18. package/express/src/framework/database/connection.ts +52 -27
  19. package/express/src/framework/events/dispatcher.ts +61 -49
  20. package/express/src/framework/facade.ts +46 -3
  21. package/express/src/framework/http/app.ts +28 -22
  22. package/express/src/framework/http/logger.ts +27 -16
  23. package/express/src/framework/http/openapi.ts +9 -11
  24. package/express/src/framework/modules/routes.ts +26 -21
  25. package/express/src/framework/notification/channels/broadcast.ts +15 -0
  26. package/express/src/framework/notification/channels/mail.ts +40 -0
  27. package/express/src/framework/notification/contracts.ts +29 -0
  28. package/express/src/framework/notification/index.ts +19 -94
  29. package/express/src/framework/notification/registry.ts +15 -0
  30. package/express/src/framework/providers/adapter.ts +27 -17
  31. package/express/src/framework/queue/queue.ts +43 -44
  32. package/express/src/framework/realtime/broadcast.ts +29 -5
  33. package/express/src/framework/realtime/socket.ts +149 -119
  34. package/express/src/framework/redis/client.ts +148 -151
  35. package/express/src/framework/runtime/server-adapter.ts +39 -34
  36. package/express/src/framework/scheduler/lock.ts +128 -116
  37. package/express/src/framework/server.ts +33 -49
  38. package/express/src/framework/session/session.ts +69 -14
  39. package/express/src/framework/storage/storage.ts +744 -785
  40. package/express/src/framework/support/mail.ts +15 -7
  41. package/express/src/providers/notification.ts +4 -0
  42. package/express/src/resources/src/components/Checkbox.vue +32 -29
  43. package/express/src/resources/src/components/Input.vue +243 -223
  44. package/express/src/resources/src/components/InputGroup.vue +147 -145
  45. package/express/src/resources/src/components/InputPasswordToggle.vue +229 -213
  46. package/express/src/resources/src/components/Modal.vue +71 -55
  47. package/express/src/resources/src/components/Refresh.vue +19 -0
  48. package/express/src/resources/src/helpers/assets.ts +51 -0
  49. package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
  50. package/express/src/resources/src/main.ts +3 -1
  51. package/express/src/resources/src/plugins/gum.ts +1 -1
  52. package/express/src/resources/src/plugins/pulse.ts +157 -147
  53. package/express/src/resources/src/stores/auth.ts +184 -164
  54. package/express/src/resources/vite.config.ts +54 -36
  55. package/express/src/storage/database/nexwire.sqlite +0 -0
  56. package/express/src/storage/logs/app.log +0 -0
  57. package/express/src/storage/logs/fatal.log +0 -0
  58. package/express/src/types/global.d.ts +32 -26
  59. package/hono/.env +1 -1
  60. package/hono/.vscode/extensions.json +6 -0
  61. package/hono/.vscode/settings.json +37 -26
  62. package/hono/biome.json +25 -2
  63. package/hono/package.json +25 -23
  64. package/hono/src/config/database.ts +48 -29
  65. package/hono/src/config/index.ts +83 -83
  66. package/hono/src/config/openapi.ts +6 -24
  67. package/hono/src/config/queue.ts +30 -30
  68. package/hono/src/config/validate.ts +50 -40
  69. package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
  70. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  71. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  72. package/hono/src/database/schema.ts +2 -2
  73. package/hono/src/framework/cache/cache.ts +40 -8
  74. package/hono/src/framework/circuit-breaker/cb.ts +88 -60
  75. package/hono/src/framework/database/connection.ts +52 -27
  76. package/hono/src/framework/events/dispatcher.ts +61 -49
  77. package/hono/src/framework/facade.ts +38 -1
  78. package/hono/src/framework/http/openapi.ts +7 -9
  79. package/hono/src/framework/notification/channels/broadcast.ts +15 -0
  80. package/hono/src/framework/notification/channels/mail.ts +40 -0
  81. package/hono/src/framework/notification/contracts.ts +29 -0
  82. package/hono/src/framework/notification/index.ts +19 -94
  83. package/hono/src/framework/notification/registry.ts +15 -0
  84. package/hono/src/framework/providers/adapter.ts +27 -17
  85. package/hono/src/framework/queue/queue.ts +43 -44
  86. package/hono/src/framework/realtime/broadcast.ts +29 -5
  87. package/hono/src/framework/realtime/socket.ts +217 -196
  88. package/hono/src/framework/redis/client.ts +148 -151
  89. package/hono/src/framework/scheduler/lock.ts +128 -116
  90. package/hono/src/framework/server.ts +27 -29
  91. package/hono/src/framework/session/session.ts +62 -14
  92. package/hono/src/framework/storage/storage.ts +744 -785
  93. package/hono/src/framework/support/mail.ts +56 -48
  94. package/hono/src/providers/notification.ts +4 -0
  95. package/hono/src/resources/src/components/Checkbox.vue +32 -29
  96. package/hono/src/resources/src/components/Input.vue +243 -234
  97. package/hono/src/resources/src/components/InputGroup.vue +147 -149
  98. package/hono/src/resources/src/components/InputPasswordToggle.vue +229 -222
  99. package/hono/src/resources/src/components/Modal.vue +71 -55
  100. package/hono/src/resources/src/components/Refresh.vue +19 -0
  101. package/hono/src/resources/src/helpers/assets.ts +51 -0
  102. package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
  103. package/hono/src/resources/src/main.ts +3 -1
  104. package/hono/src/resources/src/plugins/gum.ts +1 -1
  105. package/hono/src/resources/src/plugins/pulse.ts +157 -147
  106. package/hono/src/resources/src/stores/auth.ts +184 -164
  107. package/hono/src/resources/vite.config.ts +54 -36
  108. package/hono/src/storage/database/nexwire.sqlite +0 -0
  109. package/hono/src/storage/logs/app.log +0 -0
  110. package/hono/src/storage/logs/fatal.log +0 -0
  111. package/hono/src/types/global.d.ts +32 -26
  112. package/package.json +2 -2
  113. package/src/index.js +139 -14
@@ -1,6 +1,6 @@
1
1
  import nodemailer from "nodemailer";
2
2
  import { mailConfig } from "@/config/index.js";
3
- import { CircuitBreaker } from "@/framework/circuit-breaker/cb.js";
3
+ import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
4
4
  import { logger } from "@/framework/support/logger.js";
5
5
 
6
6
  type MailPayload = {
@@ -17,7 +17,7 @@ const transport = nodemailer.createTransport({
17
17
  auth: mailConfig.username ? { user: mailConfig.username, pass: mailConfig.password } : undefined
18
18
  });
19
19
 
20
- const mailBreaker = new CircuitBreaker();
20
+ const mailBreaker = createCircuitBreaker({ name: "SMTP Mail" });
21
21
 
22
22
  export const mail = {
23
23
  /**
@@ -35,11 +35,19 @@ export const mail = {
35
35
  })
36
36
  );
37
37
  } catch (error) {
38
- logger.error("Mail send failed", {
39
- to: payload.to,
40
- subject: payload.subject,
41
- error: error instanceof Error ? error.message : error
42
- });
38
+ // Distinguish between a Circuit Breaker rejection and an actual SMTP failure
39
+ if (isCircuitBreakerError(error)) {
40
+ logger.warn("Mail send skipped: Circuit breaker is open", {
41
+ to: payload.to,
42
+ subject: payload.subject
43
+ });
44
+ } else {
45
+ logger.error("Mail send failed", {
46
+ to: payload.to,
47
+ subject: payload.subject,
48
+ error: error instanceof Error ? error.message : error
49
+ });
50
+ }
43
51
 
44
52
  if (!mailConfig.failSilent) throw error;
45
53
  return null;
@@ -0,0 +1,4 @@
1
+ import { registerChannel, mailChannel, broadcastChannel } from "@/framework/facade.js";
2
+
3
+ registerChannel(mailChannel);
4
+ registerChannel(broadcastChannel);
@@ -1,29 +1,32 @@
1
- <template>
2
- <div class="form-check mb-0" :class="props.topclass">
3
- <input
4
- :id="String(props.id)"
5
- v-model="model"
6
- class="form-check-input shadow-none border-secondary"
7
- type="checkbox"
8
- v-bind="$attrs" />
9
- <label v-if="props.label" class="form-check-label" :for="String(props.id)">{{ props.label }}</label>
10
- </div>
11
- </template>
12
-
13
- <script setup lang="ts">
14
- defineOptions({ name: "Checkbox", inheritAttrs: false });
15
-
16
- interface CheckboxProps {
17
- id?: string | number;
18
- label?: string;
19
- topclass?: string;
20
- }
21
-
22
- const props = defineProps<CheckboxProps>();
23
-
24
- type CheckboxModel = boolean | Array<string | number>;
25
-
26
- const model = defineModel<CheckboxModel>({ default: false });
27
- </script>
28
-
29
- <style lang="scss" scoped></style>
1
+ <template>
2
+ <div class="form-check mb-0" :class="props.topclass">
3
+ <input
4
+ :id="String(props.id)"
5
+ v-model="model"
6
+ class="form-check-input shadow-none"
7
+ type="checkbox"
8
+ v-bind="$attrs"
9
+ />
10
+ <label v-if="props.label" class="form-check-label" :for="String(props.id)">{{
11
+ props.label
12
+ }}</label>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ defineOptions({ name: "Checkbox", inheritAttrs: false });
18
+
19
+ interface CheckboxProps {
20
+ id?: string | number;
21
+ label?: string;
22
+ topclass?: string;
23
+ }
24
+
25
+ const props = defineProps<CheckboxProps>();
26
+
27
+ type CheckboxModel = boolean | Array<string | number>;
28
+
29
+ const model = defineModel<CheckboxModel>({ default: false });
30
+ </script>
31
+
32
+ <style lang="scss" scoped></style>
@@ -1,223 +1,243 @@
1
- <template>
2
- <div :class="topclass">
3
- <div class="position-relative">
4
- <label
5
- :for="inputId"
6
- class="d-flex align-items-center mb-1"
7
- :class="{ 'd-none': !inputLabel }">
8
- <div class="position-relative">
9
- <span class="text-capitalize" v-html="inputLabel"></span>
10
- <span
11
- class="position-absolute text-danger rounded-circle bg-danger must"
12
- :class="{ 'd-none': !props.must }"></span>
13
- </div>
14
- <div
15
- class="text-end ms-auto text-primary fw-semibold"
16
- :class="{ 'd-none': !props.hood }"
17
- style="font-size: 12px; margin-top: 0.15rem"
18
- v-html="hoodHtml"></div>
19
- </label>
20
- <input
21
- ref="inputRef"
22
- class="form-control"
23
- :value="props.modelValue"
24
- :maxlength="maxLengthAttr"
25
- :readonly="props.readonly"
26
- v-bind="$attrs"
27
- :autocomplete="inputAutocomplete"
28
- @input="updateModel" />
29
- <div
30
- class="position-absolute bottom-0 end-0 pe-1 text-secondary max"
31
- :class="{ 'd-none': !max }">
32
- {{ maxCounter }}
33
- </div>
34
- </div>
35
- <div
36
- :id="`${inputId}Help`"
37
- class="form-text mt-1 text-danger"
38
- :class="{ 'd-none': !props.err }">
39
- {{ props.err }}
40
- </div>
41
- </div>
42
- </template>
43
-
44
- <script setup lang="ts">
45
- import { computed, nextTick, onMounted, ref, useAttrs } from "vue";
46
- import { browserDetect } from "@/plugins/browserDetect";
47
-
48
- const { isFirefox } = browserDetect;
49
-
50
- const $attrs = useAttrs();
51
- const inputRef = ref<HTMLInputElement | null>(null);
52
- type InputCategory = "number" | "decimal" | "+decimal-" | "mobile" | "tax-period" | "bin" | "challan-number" | string;
53
-
54
- interface InputProps {
55
- focus?: boolean;
56
- must?: boolean;
57
- err?: string | boolean;
58
- modelValue?: string | number;
59
- category?: InputCategory;
60
- hood?: string | boolean | number;
61
- readonly?: boolean;
62
- }
63
-
64
- const props = withDefaults(defineProps<InputProps>(), {
65
- err: false,
66
- modelValue: ""
67
- });
68
-
69
- const topclass = computed(() => ($attrs.topclass as string | undefined) || "mb-2");
70
- const inputId = computed(() => ($attrs.id as string | undefined) || "");
71
- const inputLabel = computed(() => ($attrs.label as string | undefined) || "");
72
- const inputType = computed(() => ($attrs.type as string | undefined) || "text");
73
-
74
- const max = computed<number | null>(() => {
75
- const raw = $attrs.maxlength as string | number | undefined;
76
- return raw != null ? Number(raw) : null;
77
- });
78
-
79
- const inputAutocomplete = computed(() => {
80
- const attrAutocomplete = $attrs.autocomplete as string | undefined;
81
- if (attrAutocomplete) {
82
- return attrAutocomplete;
83
- }
84
- return ["number", "decimal"].includes(props.category ?? "") ? "off" : "on";
85
- });
86
-
87
- const maxLengthAttr = computed<number | undefined>(() => max.value ?? undefined);
88
-
89
- const hoodHtml = computed(() => (props.hood === false || props.hood == null ? "" : String(props.hood)));
90
-
91
- const maxCounter = computed(() => {
92
- if (max.value == null) {
93
- return "";
94
- }
95
-
96
- if (!isFirefox && inputType.value === "month") {
97
- return "";
98
- }
99
-
100
- return max.value - String(props.modelValue).length;
101
- });
102
-
103
- const emit = defineEmits<(event: "update:modelValue", value: string | number) => void>();
104
- const updateModel = (e: Event) => {
105
- const target = e.target as HTMLInputElement;
106
-
107
- if (props.category === "number") {
108
- emit("update:modelValue", (target.value = target.value.replace(/[^\d]/gi, "")));
109
- } else if (props.category === "decimal") {
110
- let val = target.value;
111
-
112
- // Remove everything except digits, decimal point, and minus sign
113
- val = val.replace(/[^0-9.]/g, "");
114
-
115
- // Allow only one decimal point
116
- const parts = val.split(".");
117
- if (parts.length > 2) {
118
- val = `${parts[0]}.${parts.slice(1).join("")}`;
119
- }
120
-
121
- emit("update:modelValue", (target.value = val));
122
- } else if (props.category === "+decimal-") {
123
- let val = target.value;
124
-
125
- // Remove everything except digits, decimal point, and minus sign
126
- val = val.replace(/[^0-9.-]/g, ""); // if need minus sign
127
-
128
- // Move minus sign to the front if it exists anywhere
129
- const isNegative = val.includes("-");
130
- val = val.replace(/-/g, ""); // remove all minus signs
131
- if (isNegative) {
132
- val = `-${val}`;
133
- }
134
-
135
- // Allow only one decimal point
136
- const parts = val.split(".");
137
- if (parts.length > 2) {
138
- val = `${parts[0]}.${parts.slice(1).join("")}`;
139
- }
140
-
141
- emit("update:modelValue", (target.value = val));
142
- } else if (props.category === "mobile") {
143
- let digits = target.value.replace(/[^\d]/g, ""); // only digits
144
- let val = digits;
145
- if (target.value.includes("+")) {
146
- val = `+${digits}`;
147
- }
148
- emit("update:modelValue", (target.value = val));
149
- } else if (props.category === "tax-period") {
150
- let val = target.value.replace(/[^\d-]/g, ""); // Only numbers and hyphen
151
- val = val.replace(/-/g, ""); // Remove all hyphens
152
- val = val.slice(0, 6); // Only 6 digits (YYYYMM)
153
-
154
- if (val.length > 4) {
155
- let year = val.slice(0, 4);
156
- let month = val.slice(4, 6);
157
-
158
- // Only correct if both month digits entered
159
- if (month.length === 2) {
160
- let m = parseInt(month, 10);
161
- // Clamp month between 1 and 12
162
- if (m < 1) m = 1;
163
- if (m > 12) m = 12;
164
- // Always pad to 2 digits
165
- month = m.toString().padStart(2, "0");
166
- }
167
-
168
- val = `${year}-${month}`;
169
- }
170
-
171
- emit("update:modelValue", (target.value = val));
172
- } else if (props.category === "bin") {
173
- let digits = target.value.replace(/[^\d]/g, "");
174
- digits = digits.slice(0, 14);
175
- let formatted = digits;
176
- if (digits.length > 9) {
177
- formatted = `${digits.slice(0, 9)}-${digits.slice(9, 14)}`;
178
- }
179
- emit("update:modelValue", (target.value = formatted));
180
- } else if (props.category === "echallan-number") {
181
- // Format: V + 9 digits (V000517226)
182
- let raw = (target.value || "").toUpperCase();
183
-
184
- // keep only digits, but allow V (we’ll force V at start)
185
- let digits = raw.replace(/[^\d]/g, "").slice(0, 9);
186
-
187
- // always prefix V
188
- let formatted = "V" + digits;
189
-
190
- emit("update:modelValue", (target.value = formatted));
191
- } else if (props.category === "challan-number") {
192
- let digits = target.value.replace(/[^\d]/g, "");
193
- digits = digits.slice(0, 16);
194
- let formatted = digits;
195
- if (digits.length > 4) {
196
- formatted = `${digits.slice(0, 4)}-${digits.slice(4, 16)}`;
197
- }
198
- emit("update:modelValue", (target.value = formatted));
199
- } else {
200
- emit("update:modelValue", target.value);
201
- }
202
- };
203
-
204
- onMounted(() => {
205
- const shouldFocus = props.focus !== false && props.focus !== undefined;
206
- if (!shouldFocus) return;
207
-
208
- nextTick(() => inputRef.value?.focus());
209
- });
210
- </script>
211
-
212
- <style lang="scss" scoped>
213
- .must {
214
- width: 4px;
215
- height: 4px;
216
- margin-left: 2px;
217
- margin-top: 5px;
218
- }
219
-
220
- .max {
221
- font-size: 0.6rem;
222
- }
223
- </style>
1
+ <template>
2
+ <div :class="topclass">
3
+ <div class="position-relative">
4
+ <label
5
+ :for="inputId"
6
+ class="d-flex align-items-center mb-1"
7
+ :class="{ 'd-none': !inputLabel }"
8
+ >
9
+ <div class="position-relative">
10
+ <span class="text-capitalize" v-html="inputLabel"></span>
11
+ <span
12
+ class="position-absolute text-danger rounded-circle bg-danger must"
13
+ :class="{ 'd-none': !props.must }"
14
+ ></span>
15
+ </div>
16
+ <div
17
+ class="text-end ms-auto text-primary fw-semibold"
18
+ :class="{ 'd-none': !props.hood }"
19
+ style="font-size: 12px; margin-top: 0.15rem"
20
+ v-html="hoodHtml"
21
+ ></div>
22
+ </label>
23
+ <input
24
+ ref="inputRef"
25
+ class="form-control shadow-none"
26
+ :value="props.modelValue"
27
+ :maxlength="maxLengthAttr"
28
+ :readonly="props.readonly"
29
+ v-bind="$attrs"
30
+ :autocomplete="inputAutocomplete"
31
+ @input="updateModel"
32
+ />
33
+ <div
34
+ class="position-absolute bottom-0 end-0 pe-1 text-secondary max"
35
+ :class="{ 'd-none': !max }"
36
+ >
37
+ {{ maxCounter }}
38
+ </div>
39
+ </div>
40
+ <div
41
+ :id="`${inputId}Help`"
42
+ class="form-text mt-1 text-danger"
43
+ :class="{ 'd-none': !props.err }"
44
+ >
45
+ {{ props.err }}
46
+ </div>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { computed, nextTick, onMounted, ref, useAttrs } from "vue";
52
+ import { browserDetect } from "@/plugins/browserDetect";
53
+
54
+ defineOptions({ name: "Input", inheritAttrs: false });
55
+
56
+ const { isFirefox } = browserDetect;
57
+
58
+ const $attrs = useAttrs();
59
+ const inputRef = ref<HTMLInputElement | null>(null);
60
+ type InputCategory =
61
+ | "number"
62
+ | "decimal"
63
+ | "+decimal-"
64
+ | "mobile"
65
+ | "tax-period"
66
+ | "bin"
67
+ | "challan-number"
68
+ | string;
69
+
70
+ interface InputProps {
71
+ focus?: boolean;
72
+ must?: boolean;
73
+ err?: string | boolean;
74
+ modelValue?: string | number;
75
+ category?: InputCategory;
76
+ hood?: string | boolean | number;
77
+ readonly?: boolean;
78
+ }
79
+
80
+ const props = withDefaults(defineProps<InputProps>(), {
81
+ err: false,
82
+ modelValue: "",
83
+ });
84
+
85
+ const topclass = computed(() => ($attrs.topclass as string | undefined) || "mb-2");
86
+ const inputId = computed(() => ($attrs.id as string | undefined) || "");
87
+ const inputLabel = computed(() => ($attrs.label as string | undefined) || "");
88
+ const inputType = computed(() => ($attrs.type as string | undefined) || "text");
89
+
90
+ const max = computed<number | null>(() => {
91
+ const raw = $attrs.maxlength as string | number | undefined;
92
+ return raw != null ? Number(raw) : null;
93
+ });
94
+
95
+ const inputAutocomplete = computed(() => {
96
+ const attrAutocomplete = $attrs.autocomplete as string | undefined;
97
+ if (attrAutocomplete) {
98
+ return attrAutocomplete;
99
+ }
100
+ return ["number", "decimal"].includes(props.category ?? "") ? "off" : "on";
101
+ });
102
+
103
+ const maxLengthAttr = computed<number | undefined>(() => max.value ?? undefined);
104
+
105
+ const hoodHtml = computed(() =>
106
+ props.hood === false || props.hood == null ? "" : String(props.hood),
107
+ );
108
+
109
+ const maxCounter = computed(() => {
110
+ if (max.value == null) {
111
+ return "";
112
+ }
113
+
114
+ if (!isFirefox && inputType.value === "month") {
115
+ return "";
116
+ }
117
+
118
+ return max.value - String(props.modelValue).length;
119
+ });
120
+
121
+ const emit = defineEmits<(event: "update:modelValue", value: string | number) => void>();
122
+ const updateModel = (e: Event) => {
123
+ const target = e.target as HTMLInputElement;
124
+
125
+ if (props.category === "number") {
126
+ emit("update:modelValue", (target.value = target.value.replace(/[^\d]/gi, "")));
127
+ } else if (props.category === "decimal") {
128
+ let val = target.value;
129
+
130
+ // Remove everything except digits, decimal point, and minus sign
131
+ val = val.replace(/[^0-9.]/g, "");
132
+
133
+ // Allow only one decimal point
134
+ const parts = val.split(".");
135
+ if (parts.length > 2) {
136
+ val = `${parts[0]}.${parts.slice(1).join("")}`;
137
+ }
138
+
139
+ emit("update:modelValue", (target.value = val));
140
+ } else if (props.category === "+decimal-") {
141
+ let val = target.value;
142
+
143
+ // Remove everything except digits, decimal point, and minus sign
144
+ val = val.replace(/[^0-9.-]/g, ""); // if need minus sign
145
+
146
+ // Move minus sign to the front if it exists anywhere
147
+ const isNegative = val.includes("-");
148
+ val = val.replace(/-/g, ""); // remove all minus signs
149
+ if (isNegative) {
150
+ val = `-${val}`;
151
+ }
152
+
153
+ // Allow only one decimal point
154
+ const parts = val.split(".");
155
+ if (parts.length > 2) {
156
+ val = `${parts[0]}.${parts.slice(1).join("")}`;
157
+ }
158
+
159
+ emit("update:modelValue", (target.value = val));
160
+ } else if (props.category === "mobile") {
161
+ let digits = target.value.replace(/[^\d]/g, ""); // only digits
162
+ let val = digits;
163
+ if (target.value.includes("+")) {
164
+ val = `+${digits}`;
165
+ }
166
+ emit("update:modelValue", (target.value = val));
167
+ } else if (props.category === "tax-period") {
168
+ let val = target.value.replace(/[^\d-]/g, ""); // Only numbers and hyphen
169
+ val = val.replace(/-/g, ""); // Remove all hyphens
170
+ val = val.slice(0, 6); // Only 6 digits (YYYYMM)
171
+
172
+ if (val.length > 4) {
173
+ let year = val.slice(0, 4);
174
+ let month = val.slice(4, 6);
175
+
176
+ // Only correct if both month digits entered
177
+ if (month.length === 2) {
178
+ let m = parseInt(month, 10);
179
+ // Clamp month between 1 and 12
180
+ if (m < 1) m = 1;
181
+ if (m > 12) m = 12;
182
+ // Always pad to 2 digits
183
+ month = m.toString().padStart(2, "0");
184
+ }
185
+
186
+ val = `${year}-${month}`;
187
+ }
188
+
189
+ emit("update:modelValue", (target.value = val));
190
+ } else if (props.category === "bin") {
191
+ let digits = target.value.replace(/[^\d]/g, "");
192
+ digits = digits.slice(0, 14);
193
+ let formatted = digits;
194
+ if (digits.length > 9) {
195
+ formatted = `${digits.slice(0, 9)}-${digits.slice(9, 14)}`;
196
+ }
197
+ emit("update:modelValue", (target.value = formatted));
198
+ } else if (props.category === "echallan-number") {
199
+ // Format: V + 9 digits (V000517226)
200
+ let raw = (target.value || "").toUpperCase();
201
+
202
+ // keep only digits, but allow V (we’ll force V at start)
203
+ let digits = raw.replace(/[^\d]/g, "").slice(0, 9);
204
+
205
+ // always prefix V
206
+ let formatted = `V${digits}`;
207
+
208
+ emit("update:modelValue", (target.value = formatted));
209
+ } else if (props.category === "challan-number") {
210
+ let digits = target.value.replace(/[^\d]/g, "");
211
+ digits = digits.slice(0, 16);
212
+ let formatted = digits;
213
+ if (digits.length > 4) {
214
+ formatted = `${digits.slice(0, 4)}-${digits.slice(4, 16)}`;
215
+ }
216
+ emit("update:modelValue", (target.value = formatted));
217
+ } else {
218
+ emit("update:modelValue", target.value);
219
+ }
220
+ };
221
+
222
+ onMounted(async () => {
223
+ const shouldFocus = props.focus !== false && props.focus !== undefined;
224
+ if (!shouldFocus) return;
225
+
226
+ await nextTick();
227
+ // preventScroll: focusing inside a modal must not scroll the modal/page
228
+ inputRef.value?.focus({ preventScroll: true });
229
+ });
230
+ </script>
231
+
232
+ <style lang="scss" scoped>
233
+ .must {
234
+ width: 4px;
235
+ height: 4px;
236
+ margin-left: 2px;
237
+ margin-top: 5px;
238
+ }
239
+
240
+ .max {
241
+ font-size: 0.6rem;
242
+ }
243
+ </style>