@autobusal/utilities 1.5.5 → 1.7.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.
@@ -34,7 +34,18 @@
34
34
  import { readdirSync, readFileSync, statSync } from 'node:fs';
35
35
  import { join } from 'node:path';
36
36
 
37
- const OPEN = /styled(?:\.\w+|\([^)]*\))(?:<[^>]*>)?`/g;
37
+ /*
38
+ * Claude - 2026-08-30: `css` was missing from this, and the miss was not
39
+ * theoretical - magus's own `columnStyles = css<{...}>\`` carried a comment
40
+ * quoting a theme token in backticks, this script reported "clean", and the
41
+ * build then failed with a parse error pointing somewhere else entirely.
42
+ *
43
+ * A shared `css` block is the same template literal as `styled.x` and
44
+ * breaks in exactly the same way, so it has to be scanned the same way.
45
+ * `keyframes` and `createGlobalStyle` are here for the same reason before
46
+ * somebody finds them the hard way.
47
+ */
48
+ const OPEN = /(?:styled(?:\.\w+|\([^)]*\))|css|keyframes|createGlobalStyle)(?:<[^>]*>)?`/g;
38
49
 
39
50
  const walk = (dir) => readdirSync(dir).flatMap((entry) => {
40
51
  const path = join(dir, entry);
package/date.tsx CHANGED
@@ -102,4 +102,41 @@ export const futureDate = (days: number): string => {
102
102
  dayAfter.setDate(today.getDate() + days);
103
103
 
104
104
  return prepareDate(dayAfter);
105
- };
105
+ };
106
+ /**
107
+ * A prepared d/m/Y date as the API's own Y-m-d.
108
+ *
109
+ * Claude - 2026-11-03. The calendar fields post d/m/Y, which the endpoints
110
+ * built for them validate with date_format:d/m/Y; the ones that take a plain
111
+ * `date` (a recorded payout, a settlement) read "31/12/2026" the American
112
+ * way and refuse it. Convert at the edge; anything that is not a prepared
113
+ * date is passed through untouched.
114
+ */
115
+ export const isoDate = (prepared?: string | null): string => {
116
+ if (!isPreparedDate(prepared)) {
117
+ return prepared ?? '';
118
+ }
119
+
120
+ const [ day, month, year ] = String(prepared).split('/');
121
+
122
+ return `${ year }-${ month }-${ day }`;
123
+ };
124
+
125
+ /**
126
+ * The other direction: a date input's yyyy-mm-dd as the d/m/Y most obtapi
127
+ * list filters parse (Carbon::createFromFormat('d/m/Y')).
128
+ *
129
+ * Claude - 2026-11-04 (dashboard simplification, wave 7). The shared Filters
130
+ * toolbar writes native date inputs (yyyy-mm-dd) into the URL; the invoice
131
+ * and API-log endpoints still read d/m/Y. Anything that is not yyyy-mm-dd -
132
+ * an old d/m/Y bookmark, an empty value - passes through untouched.
133
+ */
134
+ export const dmyDate = (value?: string | null): string => {
135
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value ?? '');
136
+
137
+ if (match === null) {
138
+ return value ?? '';
139
+ }
140
+
141
+ return `${ match[3] }/${ match[2] }/${ match[1] }`;
142
+ };
package/form.tsx CHANGED
@@ -94,4 +94,4 @@ export const Display = (err: (FieldError | Merge<FieldError, FieldErrorsImpl<any
94
94
  { err.message as string }
95
95
  </Error>
96
96
  );
97
- }
97
+ }
package/index.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { createDate, prepareDate, isPreparedDate, getFormattedShort, getDayParts, differenceYears, futureDate } from './date';
1
+ import { createDate, prepareDate, isPreparedDate, isoDate, dmyDate, getFormattedShort, getDayParts, differenceYears, futureDate } from './date';
2
2
  import { Validate, Display } from './form';
3
+ import { serverErrors } from './serverErrors';
3
4
  import { error, notify, success } from './notify';
4
5
  import { getAllowed, getLanguages, getPayments } from './options';
5
6
  import { getSex, getOptionsSex } from './passengers';
@@ -12,12 +13,15 @@ export {
12
13
  sanitizeHtml,
13
14
  Validate,
14
15
  Display,
16
+ serverErrors,
15
17
  error,
16
18
  notify,
17
19
  success,
18
20
  createDate,
19
21
  prepareDate,
20
22
  isPreparedDate,
23
+ isoDate,
24
+ dmyDate,
21
25
  getFormattedShort,
22
26
  getDayParts,
23
27
  differenceYears,
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@autobusal/utilities",
3
- "version": "1.5.5",
3
+ "version": "1.7.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "dependencies": {
8
8
  "dompurify": "^3.4.14"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
9
12
  }
10
13
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The field errors of a failed save, keyed by field.
3
+ *
4
+ * Claude - 2026-11-03 (dashboard simplification, wave 0). Laravel answers a
5
+ * rejected form with 422 and `{ message, errors: { field: [text] } }`; the
6
+ * API client toasts the message and the fields stayed silent, so the admin
7
+ * had to guess which of forty inputs the server meant. Pure: an Axios error
8
+ * in, a flat map out (the first message per field - one is enough under an
9
+ * input), an empty map for anything that is not a 422. Viewer takes it as
10
+ * `errors` and puts each message under its own field.
11
+ *
12
+ * A nested Laravel key (title.en) is passed through unchanged -
13
+ * react-hook-form resolves dotted names itself.
14
+ */
15
+ export const serverErrors = (error: unknown): Record<string, string> => {
16
+ const response = (error as { response?: { status?: number, data?: { errors?: Record<string, string[] | string> } } } | null | undefined)?.response;
17
+
18
+ if (response === undefined || response.status !== 422 || response.data?.errors === undefined || response.data.errors === null) {
19
+ return {};
20
+ }
21
+
22
+ return Object.fromEntries(
23
+ Object.entries(response.data.errors)
24
+ .map(([ field, messages ]) => [ field, Array.isArray(messages) ? String(messages[0] ?? '') : String(messages) ])
25
+ .filter(([ , message ]) => message !== '')
26
+ );
27
+ };
package/CHANGELOG.md DELETED
@@ -1,83 +0,0 @@
1
- # Changelog
2
-
3
- ## 1.5.5 (2026-08-29)
4
-
5
- - New isPreparedDate(): whether a string is a d/m/Y date the app can work with - createDate turns anything else into an Invalid Date, which is how the search URL's literal 'none' produced a January calendar.
6
-
7
- ## 1.5.4 (2026-08-28)
8
-
9
- - DisplayStatus renders status 4 (awaiting approval) as a badge instead of a blank cell in the admin agent/visitor/API-consumer lists (sweep #61).
10
-
11
- ## 1.5.3
12
-
13
- ### Fixed
14
-
15
- - **The backtick guard tripped on prose about styled-components**, and was refusing to publish the whole repo. Its opening pattern matches `styled.x` followed by a backtick as *text*, so a JSDoc line describing another file - "labels as `<span>` (Viewer/styles.ts: `Label = styled.span`)" - opened a template no code had opened, and every comment up to the next `` `; `` in the file counted as inside it. A comment about the hazard tripping the guard against the hazard. Openings found inside comments are now ignored; backticks inside comments, which are the thing being hunted, still are not.
16
-
17
- ## 1.5.2
18
-
19
- ### Changed
20
-
21
- - DOMPurify raised to `^3.4.14` for GHSA-55q2-fjhq-7xh7. It rates as moderate, which understates it here: DOMPurify is what `sanitizeHtml` is, so it sits directly on the path every piece of admin-authored and model-generated HTML takes before reaching a browser.
22
-
23
- ## 1.5.1
24
-
25
- ### Changed
26
-
27
- - **`error` collapses repeats into one toast** - it now raises the toast
28
- under an id, defaulting to the message itself, so a page whose parallel
29
- queries all fail the same way shows one red box rather than a stack of
30
- identical ones. Existing call sites need no change. A repeat of the same
31
- failure once the toast has gone still raises a fresh one.
32
-
33
- ## 1.5.0
34
-
35
- ### Added
36
-
37
- - **`GetStatus` knows status 4, "awaiting approval"** - distinct from 3
38
- (pending, waiting on the account to finish its own form) and from 2
39
- (suspended, a decision already taken against it).
40
-
41
-
42
- ## 1.4.1
43
-
44
- ### Changed
45
-
46
- - **Gender can be "not declared".** `getOptionsSex` gains it as the first
47
- option and `getSex` reports it, instead of treating everything that is not
48
- `1` as male — which is what three existing rows with no value were already
49
- rendering as.
50
-
51
- Carried by `null`: the `visitors`.`sex` column has always been nullable, so
52
- nothing migrated. The option's id is `''`, which Laravel's
53
- `ConvertEmptyStringsToNull` turns into a real null on the way in, so "not
54
- declared" is the *absence* of a value at every layer rather than a magic
55
- number each one has to learn.
56
-
57
- Deliberately not shared with the checkout passenger forms — those build
58
- their own field, and gender there is a per-route requirement a carrier can
59
- insist on. A profile is the person; a passenger record is what a carrier
60
- demands in order to board them.
61
-
62
- ## 1.4.0
63
-
64
- ### Added
65
-
66
- - **`check-backticks.mjs`**, moved here from the autobusal repo root so the
67
- apps can run it too.
68
-
69
- A backtick used as quoting inside a styled-components template *terminates
70
- the template*. The bundler then reports a syntax error somewhere further
71
- down, pointing at a line that is perfectly fine — so the search starts in
72
- the wrong place every time. Per the script's own history that cost five
73
- build failures in one session before anyone connected the two.
74
-
75
- It only ever guarded `packages`, and the trap is not confined there: it bit
76
- `magus/src` the moment a public page grew its own styled-components, with
77
- nothing watching. Each app's `prebuild` now runs it over its own `src`
78
- (first, so a broken template stops the build before it spends minutes
79
- copying files), and `publish.sh` still runs it over `packages`.
80
-
81
- One script with three callers, rather than a copy per repo that drifts.
82
-
83
- Takes the directory to scan as its argument.