Commit fc62d6d7a693f621909a9c7489122a937001708d
1 parent
db8d7b51
feat(web+distribution): R1 Web SPA bootstrap + demo seed
First runnable end-to-end demo: open the browser, log in, click
through every PBC, and walk a sales order DRAFT → CONFIRMED →
SHIPPED. Stock balances drop, the SALES_SHIPMENT row appears in the
ledger, and the AR journal entry settles — all visible in the SPA
without touching curl.
Bumps version to 0.29.0-SNAPSHOT.
What landed
-----------
* New `:web` Gradle subproject — Vite + React 18 + TypeScript +
Tailwind 3.4. The Gradle wrapper is two `Exec` tasks
(`npmInstall`, `npmBuild`) with proper inputs/outputs declared
for incremental builds. Deliberately no node-gradle plugin —
one less moving piece.
* SPA architecture: hand-written typed REST client over `fetch`
(auth header injection + 401 handler), AuthContext that decodes
the JWT for display, ProtectedRoute, AppLayout with sidebar
grouped by PBC, 16 page components covering the full v1 surface
(Items, UoMs, Partners, Locations, Stock Balances + Movements,
Sales Orders + detail w/ confirm/ship/cancel, Purchase Orders +
detail w/ confirm/receive/cancel, Work Orders + detail w/
start/complete, Shop-Floor dashboard with 5s polling, Journal
Entries). 211 KB JS / 21 KB CSS gzipped.
* Sales-order detail page: confirm/ship/cancel verbs each refresh
the order, the (SO-filtered) movements list, and the
(SO-filtered) journal entries — so an operator watches the
ledger row appear and the AR row settle in real time after a
single click. Same pattern on the purchase-order detail page
for the AP/RECEIPT side.
* Shop-floor dashboard polls /api/v1/production/work-orders/shop-
floor every 5s and renders one card per IN_PROGRESS WO with
current operation, planned vs actual minutes (progress bar),
and operations-completed.
* `:distribution` consumes the SPA dist via a normal Gradle
outgoing/incoming configuration: `:web` exposes
`webStaticBundle`, `:distribution`'s `bundleWebStatic` Sync
task copies it into `${buildDir}/web-static/static/`, and
that parent directory is added to the main resources source
set so Spring Boot serves the SPA from `classpath:/static/`
out of the same fat-jar. Single artifact, no nginx, no CORS.
* New `SpaController` in platform-bootstrap forwards every
known SPA route prefix to `/index.html` so React Router's
HTML5 history mode works on hard refresh / deep-link entry.
Explicit list (12 prefixes) rather than catch-all so typoed
API URLs still get an honest 404 instead of the SPA shell.
* SecurityConfiguration restructured: keeps the public allowlist
for /api/v1/auth + /api/v1/_meta + /v3/api-docs + /swagger-ui,
then `/api/**` is `.authenticated()`, then SPA static assets
+ every SPA route prefix are `.permitAll()`. The order is
load-bearing — putting `.authenticated()` for /api/** BEFORE
the SPA permitAll preserves the framework's "API is always
authenticated" invariant even with the SPA bundled in the
same fat-jar. The SPA bundle itself is just HTML+CSS+JS so
permitting it is correct; secrets are gated by /api/**.
* New `DemoSeedRunner` in `:distribution` (gated behind
`vibeerp.demo.seed=true`, set in application-dev.yaml only).
Idempotent — the runner short-circuits if its sentinel item
(DEMO-PAPER-A4) already exists. Seeds 5 items, 2 warehouses,
4 partners, opening stock for every item, one open
DEMO-SO-0001 (50× business cards + 20× brochures, $720), one
open DEMO-PO-0001 (10000× paper, $400). Every row carries
the DEMO- prefix so it's trivially distinguishable from
hand-created data; a future "delete demo data" command has
an obvious filter. Production deploys never set the property,
so the @ConditionalOnProperty bean stays absent from the
context.
How to run
----------
docker compose up -d db
./gradlew :distribution:bootRun
open http://localhost:8080
# Read the bootstrap admin password from the boot log,
# log in as admin, and walk DEMO-SO-0001 through the
# confirm + ship flow to see the buy-sell loop in the UI.
What was caught by the smoke test
---------------------------------
* TypeScript strict mode + `error: unknown` in React state →
`{error && <X/>}` evaluates to `unknown` and JSX rejects it.
Fixed by typing the state as `Error | null` and converting
in catches with `e instanceof Error ? e : new Error(String(e))`.
Affected 16 page files; the conversion is now uniform.
* DataTable's `T extends Record<string, unknown>` constraint was
too restrictive for typed row interfaces; relaxed to
unconstrained `T` with `(row as unknown as Record<…>)[key]`
for the unkeyed cell read fallback.
* `vite.config.ts` needs `@types/node` for `node:path` +
`__dirname`; added to devDependencies and tsconfig.node.json
declares `"types": ["node"]`.
* KDoc nested-comment trap (4th time): SpaController's KDoc
had `/api/v1/...` in backticks; the `/*` inside backticks
starts a nested block comment and breaks Kotlin compilation
with "Unclosed comment". Rephrased to "the api-v1 prefix".
* SecurityConfiguration order: a draft version that put the
SPA permit-all rules BEFORE `/api/**` authenticated() let
unauthenticated requests reach API endpoints. Caught by an
explicit smoke test (curl /api/v1/some-bogus-endpoint should
return 401, not 404 from a missing static file). Reordered
so /api/** authentication runs first.
End-to-end smoke (real Postgres, fresh DB)
------------------------------------------
- bootRun starts in 7.6s
- DemoSeedRunner reports "populating starter dataset… done"
- GET / returns the SPA HTML
- GET /sales-orders, /sales-orders/<uuid>, /journal-entries
all return 200 (SPA shell — React Router takes over)
- GET /assets/index-*.js / /assets/index-*.css both 200
- GET /api/v1/some-bogus-endpoint → 401 (Spring Security
rejects before any controller mapping)
- admin login via /api/v1/auth/login → 200 + JWT
- GET /catalog/items → 5 DEMO-* rows
- GET /partners/partners → 4 DEMO-* rows
- GET /inventory/locations → 2 DEMO-* warehouses
- GET /inventory/balances → 5 starting balances
- POST /orders/sales-orders/<id>/confirm → CONFIRMED;
GET /finance/journal-entries shows AR POSTED 720 USD
- POST /orders/sales-orders/<id>/ship {"shippingLocationCode":
"DEMO-WH-FG"} → SHIPPED; balances drop to 150 + 80;
journal entry flips to SETTLED
- POST /orders/purchase-orders/<id>/confirm → CONFIRMED;
AP POSTED 400 USD appears
- POST /orders/purchase-orders/<id>/receive → RECEIVED;
PAPER balance grows from 5000 to 15000; AP row SETTLED
- 8 stock_movement rows in the ledger total
Showing
46 changed files
with
6282 additions
and
8 deletions
distribution/build.gradle.kts
| @@ -4,6 +4,16 @@ | @@ -4,6 +4,16 @@ | ||
| 4 | // together into a bootable Spring Boot application. It is referenced by | 4 | // together into a bootable Spring Boot application. It is referenced by |
| 5 | // the Dockerfile (stage 1) which produces the shipping image documented | 5 | // the Dockerfile (stage 1) which produces the shipping image documented |
| 6 | // in the architecture spec, sections 10 and 11. | 6 | // in the architecture spec, sections 10 and 11. |
| 7 | +// | ||
| 8 | +// **Web SPA bundling.** The `:web` subproject is a Gradle wrapper around | ||
| 9 | +// the Vite/React build. We declare a `webStaticBundle` consumer | ||
| 10 | +// configuration against it; the `bundleWebStatic` Sync task copies | ||
| 11 | +// the dist/ directory into `${buildDir}/web-static/static/` and that | ||
| 12 | +// parent directory is added to the main resources source set so Spring | ||
| 13 | +// Boot's `classpath:/static/**` resource resolver picks the SPA up at | ||
| 14 | +// runtime. The result is a single fat-jar containing both the JVM | ||
| 15 | +// backend and the SPA — no nginx, no separate static-file server, no | ||
| 16 | +// CORS. | ||
| 7 | 17 | ||
| 8 | plugins { | 18 | plugins { |
| 9 | alias(libs.plugins.kotlin.jvm) | 19 | alias(libs.plugins.kotlin.jvm) |
| @@ -89,4 +99,45 @@ tasks.bootRun { | @@ -89,4 +99,45 @@ tasks.bootRun { | ||
| 89 | // <repo>/plugins-dev/, not <repo>/distribution/plugins-dev/. | 99 | // <repo>/plugins-dev/, not <repo>/distribution/plugins-dev/. |
| 90 | workingDir = rootProject.layout.projectDirectory.asFile | 100 | workingDir = rootProject.layout.projectDirectory.asFile |
| 91 | dependsOn(":reference-customer:plugin-printing-shop:installToDev") | 101 | dependsOn(":reference-customer:plugin-printing-shop:installToDev") |
| 102 | + // Make sure the SPA dist/ is staged into the classpath before | ||
| 103 | + // Spring Boot starts; otherwise an in-IDE bootRun with no prior | ||
| 104 | + // build serves a 404 at the root path. processResources already | ||
| 105 | + // depends on bundleWebStatic, but listing it explicitly here | ||
| 106 | + // makes the relationship visible to anyone reading bootRun. | ||
| 107 | + dependsOn("bundleWebStatic") | ||
| 108 | +} | ||
| 109 | + | ||
| 110 | +// ─── Web SPA bundling ──────────────────────────────────────────────── | ||
| 111 | +// | ||
| 112 | +// Resolvable configuration that pulls the `webStaticBundle` artifact | ||
| 113 | +// from `:web`. The `:web` subproject's outgoing configuration declares | ||
| 114 | +// the dist/ directory as its single artifact, built by the npmBuild | ||
| 115 | +// Exec task. We don't need attribute matchers because there's only | ||
| 116 | +// one artifact in the configuration. | ||
| 117 | +val webStaticBundle: Configuration by configurations.creating { | ||
| 118 | + isCanBeConsumed = false | ||
| 119 | + isCanBeResolved = true | ||
| 120 | +} | ||
| 121 | + | ||
| 122 | +dependencies { | ||
| 123 | + webStaticBundle(project(mapOf("path" to ":web", "configuration" to "webStaticBundle"))) | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +// Stage the SPA dist into ${buildDir}/web-static/static/ so the | ||
| 127 | +// directory layout matches Spring Boot's classpath:/static/ convention. | ||
| 128 | +// Adding the parent dir as a resource source set is what then makes | ||
| 129 | +// the JAR's classpath include those files at the right path. | ||
| 130 | +val bundleWebStatic by tasks.registering(Sync::class) { | ||
| 131 | + group = "build" | ||
| 132 | + description = "Stage the :web SPA bundle into the distribution resources tree." | ||
| 133 | + from({ webStaticBundle }) | ||
| 134 | + into(layout.buildDirectory.dir("web-static/static")) | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +sourceSets.named("main") { | ||
| 138 | + resources.srcDir(layout.buildDirectory.dir("web-static")) | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +tasks.named("processResources") { | ||
| 142 | + dependsOn(bundleWebStatic) | ||
| 92 | } | 143 | } |
distribution/src/main/kotlin/org/vibeerp/demo/DemoSeedRunner.kt
0 → 100644
| 1 | +package org.vibeerp.demo | ||
| 2 | + | ||
| 3 | +import org.slf4j.LoggerFactory | ||
| 4 | +import org.springframework.boot.CommandLineRunner | ||
| 5 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty | ||
| 6 | +import org.springframework.stereotype.Component | ||
| 7 | +import org.springframework.transaction.annotation.Transactional | ||
| 8 | +import org.vibeerp.pbc.catalog.application.CreateItemCommand | ||
| 9 | +import org.vibeerp.pbc.catalog.application.ItemService | ||
| 10 | +import org.vibeerp.pbc.catalog.domain.ItemType | ||
| 11 | +import org.vibeerp.pbc.inventory.application.CreateLocationCommand | ||
| 12 | +import org.vibeerp.pbc.inventory.application.LocationService | ||
| 13 | +import org.vibeerp.pbc.inventory.application.StockBalanceService | ||
| 14 | +import org.vibeerp.pbc.inventory.domain.LocationType | ||
| 15 | +import org.vibeerp.pbc.orders.purchase.application.CreatePurchaseOrderCommand | ||
| 16 | +import org.vibeerp.pbc.orders.purchase.application.PurchaseOrderLineCommand | ||
| 17 | +import org.vibeerp.pbc.orders.purchase.application.PurchaseOrderService | ||
| 18 | +import org.vibeerp.pbc.orders.sales.application.CreateSalesOrderCommand | ||
| 19 | +import org.vibeerp.pbc.orders.sales.application.SalesOrderLineCommand | ||
| 20 | +import org.vibeerp.pbc.orders.sales.application.SalesOrderService | ||
| 21 | +import org.vibeerp.pbc.partners.application.CreatePartnerCommand | ||
| 22 | +import org.vibeerp.pbc.partners.application.PartnerService | ||
| 23 | +import org.vibeerp.pbc.partners.domain.PartnerType | ||
| 24 | +import org.vibeerp.platform.persistence.security.PrincipalContext | ||
| 25 | +import java.math.BigDecimal | ||
| 26 | +import java.time.LocalDate | ||
| 27 | + | ||
| 28 | +/** | ||
| 29 | + * One-shot demo data seeder, gated behind `vibeerp.demo.seed=true`. | ||
| 30 | + * | ||
| 31 | + * **Why this exists.** Out-of-the-box, vibe_erp boots against an | ||
| 32 | + * empty Postgres and the SPA dashboard shows zeros for every PBC. | ||
| 33 | + * Onboarding a new operator (or running a tomorrow-morning demo) | ||
| 34 | + * needs a couple of minutes of clicking to create items, | ||
| 35 | + * locations, partners, and a starting inventory before the | ||
| 36 | + * interesting screens become useful. This runner stages a tiny | ||
| 37 | + * but representative dataset on first boot so the moment the | ||
| 38 | + * bootstrap admin lands on `/`, every page already has rows. | ||
| 39 | + * | ||
| 40 | + * **Opt-in by property.** `@ConditionalOnProperty` keeps this | ||
| 41 | + * bean entirely absent from production deployments — only the | ||
| 42 | + * dev profile (`application-dev.yaml` sets `vibeerp.demo.seed: | ||
| 43 | + * true`) opts in. A future release can ship a `--demo` CLI flag | ||
| 44 | + * or a one-time admin "Load demo data" button that flips the | ||
| 45 | + * same property at runtime; for v1 the dev profile is enough. | ||
| 46 | + * | ||
| 47 | + * **Idempotent.** The runner checks for one of its own seeded | ||
| 48 | + * item codes and short-circuits if already present. Restarting | ||
| 49 | + * the dev server is a no-op; deleting the demo data has to | ||
| 50 | + * happen via SQL or by dropping the DB. Idempotency on the | ||
| 51 | + * sentinel item is intentional (vs. on every entity it creates): | ||
| 52 | + * a half-seeded DB from a crashed first run will *not* recover | ||
| 53 | + * cleanly, but that case is exotic and we can clear and retry | ||
| 54 | + * in dev. | ||
| 55 | + * | ||
| 56 | + * **All seeded data shares the `DEMO-` prefix.** Items, partners, | ||
| 57 | + * locations, and order codes all start with `DEMO-`. This makes | ||
| 58 | + * the seeded data trivially distinguishable from anything an | ||
| 59 | + * operator creates by hand later — and gives a future | ||
| 60 | + * "delete demo data" command an obvious filter. | ||
| 61 | + * | ||
| 62 | + * **System principal.** Audit columns need a non-blank | ||
| 63 | + * `created_by`; `PrincipalContext.runAs("__demo_seed__")` wraps | ||
| 64 | + * the entire seed so every row carries that sentinel. The | ||
| 65 | + * authorization aspect (`@RequirePermission`) lives on | ||
| 66 | + * controllers, not services — calling services directly bypasses | ||
| 67 | + * it cleanly, which is correct for system-level seeders. | ||
| 68 | + * | ||
| 69 | + * **Why CommandLineRunner.** Equivalent to `ApplicationRunner` | ||
| 70 | + * here — there are no command-line args this seeder cares about. | ||
| 71 | + * Spring runs every CommandLineRunner once, after the application | ||
| 72 | + * context is fully initialized but before serving traffic, which | ||
| 73 | + * is exactly the right window: services are wired, the schema is | ||
| 74 | + * applied, but the first HTTP request hasn't arrived yet. | ||
| 75 | + * | ||
| 76 | + * **Lives in distribution.** This is the only module that | ||
| 77 | + * already depends on every PBC, which is what the seeder needs | ||
| 78 | + * to compose. It's gated behind a property the production | ||
| 79 | + * application.yaml never sets, so its presence in the fat-jar | ||
| 80 | + * is dormant unless explicitly opted in. | ||
| 81 | + */ | ||
| 82 | +@Component | ||
| 83 | +@ConditionalOnProperty(prefix = "vibeerp.demo", name = ["seed"], havingValue = "true") | ||
| 84 | +class DemoSeedRunner( | ||
| 85 | + private val itemService: ItemService, | ||
| 86 | + private val locationService: LocationService, | ||
| 87 | + private val stockBalanceService: StockBalanceService, | ||
| 88 | + private val partnerService: PartnerService, | ||
| 89 | + private val salesOrderService: SalesOrderService, | ||
| 90 | + private val purchaseOrderService: PurchaseOrderService, | ||
| 91 | +) : CommandLineRunner { | ||
| 92 | + | ||
| 93 | + private val log = LoggerFactory.getLogger(DemoSeedRunner::class.java) | ||
| 94 | + | ||
| 95 | + @Transactional | ||
| 96 | + override fun run(vararg args: String?) { | ||
| 97 | + if (itemService.findByCode(SENTINEL_ITEM_CODE) != null) { | ||
| 98 | + log.info("Demo seed: data already present (sentinel item {} found); skipping", SENTINEL_ITEM_CODE) | ||
| 99 | + return | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + log.info("Demo seed: populating starter dataset…") | ||
| 103 | + PrincipalContext.runAs("__demo_seed__") { | ||
| 104 | + seedItems() | ||
| 105 | + seedLocations() | ||
| 106 | + seedPartners() | ||
| 107 | + seedStock() | ||
| 108 | + seedSalesOrder() | ||
| 109 | + seedPurchaseOrder() | ||
| 110 | + } | ||
| 111 | + log.info("Demo seed: done") | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + // ─── Items ─────────────────────────────────────────────────────── | ||
| 115 | + | ||
| 116 | + private fun seedItems() { | ||
| 117 | + item(SENTINEL_ITEM_CODE, "A4 paper, 80gsm, white", ItemType.GOOD, "sheet") | ||
| 118 | + item("DEMO-INK-CYAN", "Cyan offset ink", ItemType.GOOD, "kg") | ||
| 119 | + item("DEMO-INK-MAGENTA", "Magenta offset ink", ItemType.GOOD, "kg") | ||
| 120 | + item("DEMO-CARD-BIZ", "Business cards, 100/pack", ItemType.GOOD, "pack") | ||
| 121 | + item("DEMO-BROCHURE-A5", "Folded A5 brochure", ItemType.GOOD, "ea") | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + private fun item(code: String, name: String, type: ItemType, baseUomCode: String) { | ||
| 125 | + itemService.create( | ||
| 126 | + CreateItemCommand( | ||
| 127 | + code = code, | ||
| 128 | + name = name, | ||
| 129 | + description = null, | ||
| 130 | + itemType = type, | ||
| 131 | + baseUomCode = baseUomCode, | ||
| 132 | + active = true, | ||
| 133 | + ), | ||
| 134 | + ) | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + // ─── Locations ─────────────────────────────────────────────────── | ||
| 138 | + | ||
| 139 | + private fun seedLocations() { | ||
| 140 | + location("DEMO-WH-RAW", "Raw materials warehouse") | ||
| 141 | + location("DEMO-WH-FG", "Finished goods warehouse") | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + private fun location(code: String, name: String) { | ||
| 145 | + locationService.create( | ||
| 146 | + CreateLocationCommand( | ||
| 147 | + code = code, | ||
| 148 | + name = name, | ||
| 149 | + type = LocationType.WAREHOUSE, | ||
| 150 | + active = true, | ||
| 151 | + ), | ||
| 152 | + ) | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + // ─── Partners ──────────────────────────────────────────────────── | ||
| 156 | + | ||
| 157 | + private fun seedPartners() { | ||
| 158 | + partner("DEMO-CUST-ACME", "Acme Print Co.", PartnerType.CUSTOMER, "ap@acme.example") | ||
| 159 | + partner("DEMO-CUST-GLOBE", "Globe Marketing", PartnerType.CUSTOMER, "ops@globe.example") | ||
| 160 | + partner("DEMO-SUPP-PAPERWORLD", "Paper World Ltd.", PartnerType.SUPPLIER, "sales@paperworld.example") | ||
| 161 | + partner("DEMO-SUPP-INKCO", "InkCo Industries", PartnerType.SUPPLIER, "orders@inkco.example") | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + private fun partner(code: String, name: String, type: PartnerType, email: String) { | ||
| 165 | + partnerService.create( | ||
| 166 | + CreatePartnerCommand( | ||
| 167 | + code = code, | ||
| 168 | + name = name, | ||
| 169 | + type = type, | ||
| 170 | + email = email, | ||
| 171 | + active = true, | ||
| 172 | + ), | ||
| 173 | + ) | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + // ─── Initial stock ─────────────────────────────────────────────── | ||
| 177 | + | ||
| 178 | + private fun seedStock() { | ||
| 179 | + val rawWh = locationService.findByCode("DEMO-WH-RAW")!! | ||
| 180 | + val fgWh = locationService.findByCode("DEMO-WH-FG")!! | ||
| 181 | + | ||
| 182 | + stockBalanceService.adjust(SENTINEL_ITEM_CODE, rawWh.id, BigDecimal("5000")) | ||
| 183 | + stockBalanceService.adjust("DEMO-INK-CYAN", rawWh.id, BigDecimal("50")) | ||
| 184 | + stockBalanceService.adjust("DEMO-INK-MAGENTA", rawWh.id, BigDecimal("50")) | ||
| 185 | + | ||
| 186 | + stockBalanceService.adjust("DEMO-CARD-BIZ", fgWh.id, BigDecimal("200")) | ||
| 187 | + stockBalanceService.adjust("DEMO-BROCHURE-A5", fgWh.id, BigDecimal("100")) | ||
| 188 | + } | ||
| 189 | + | ||
| 190 | + // ─── Open sales order (DRAFT — ready to confirm + ship) ────────── | ||
| 191 | + | ||
| 192 | + private fun seedSalesOrder() { | ||
| 193 | + salesOrderService.create( | ||
| 194 | + CreateSalesOrderCommand( | ||
| 195 | + code = "DEMO-SO-0001", | ||
| 196 | + partnerCode = "DEMO-CUST-ACME", | ||
| 197 | + orderDate = LocalDate.now(), | ||
| 198 | + currencyCode = "USD", | ||
| 199 | + lines = listOf( | ||
| 200 | + SalesOrderLineCommand( | ||
| 201 | + lineNo = 1, | ||
| 202 | + itemCode = "DEMO-CARD-BIZ", | ||
| 203 | + quantity = BigDecimal("50"), | ||
| 204 | + unitPrice = BigDecimal("12.50"), | ||
| 205 | + currencyCode = "USD", | ||
| 206 | + ), | ||
| 207 | + SalesOrderLineCommand( | ||
| 208 | + lineNo = 2, | ||
| 209 | + itemCode = "DEMO-BROCHURE-A5", | ||
| 210 | + quantity = BigDecimal("20"), | ||
| 211 | + unitPrice = BigDecimal("4.75"), | ||
| 212 | + currencyCode = "USD", | ||
| 213 | + ), | ||
| 214 | + ), | ||
| 215 | + ), | ||
| 216 | + ) | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + // ─── Open purchase order (DRAFT — ready to confirm + receive) ──── | ||
| 220 | + | ||
| 221 | + private fun seedPurchaseOrder() { | ||
| 222 | + purchaseOrderService.create( | ||
| 223 | + CreatePurchaseOrderCommand( | ||
| 224 | + code = "DEMO-PO-0001", | ||
| 225 | + partnerCode = "DEMO-SUPP-PAPERWORLD", | ||
| 226 | + orderDate = LocalDate.now(), | ||
| 227 | + expectedDate = LocalDate.now().plusDays(7), | ||
| 228 | + currencyCode = "USD", | ||
| 229 | + lines = listOf( | ||
| 230 | + PurchaseOrderLineCommand( | ||
| 231 | + lineNo = 1, | ||
| 232 | + itemCode = SENTINEL_ITEM_CODE, | ||
| 233 | + quantity = BigDecimal("10000"), | ||
| 234 | + unitPrice = BigDecimal("0.04"), | ||
| 235 | + currencyCode = "USD", | ||
| 236 | + ), | ||
| 237 | + ), | ||
| 238 | + ), | ||
| 239 | + ) | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + companion object { | ||
| 243 | + /** | ||
| 244 | + * The seeder uses the presence of this item as the | ||
| 245 | + * idempotency marker — re-running the seeder against a | ||
| 246 | + * Postgres that already contains it short-circuits. The | ||
| 247 | + * choice of "the very first item the seeder creates" is | ||
| 248 | + * deliberate: if the seed transaction commits at all, this | ||
| 249 | + * row is in the DB; if it doesn't, nothing is. | ||
| 250 | + */ | ||
| 251 | + const val SENTINEL_ITEM_CODE: String = "DEMO-PAPER-A4" | ||
| 252 | + } | ||
| 253 | +} |
distribution/src/main/resources/application-dev.yaml
| @@ -21,6 +21,14 @@ vibeerp: | @@ -21,6 +21,14 @@ vibeerp: | ||
| 21 | directory: ./plugins-dev | 21 | directory: ./plugins-dev |
| 22 | files: | 22 | files: |
| 23 | local-path: ./files-dev | 23 | local-path: ./files-dev |
| 24 | + demo: | ||
| 25 | + # Opt in to the one-shot DemoSeedRunner (in distribution/.../demo/) | ||
| 26 | + # which stages a tiny but representative dataset on first boot. | ||
| 27 | + # Idempotent — the runner checks for its own sentinel item before | ||
| 28 | + # creating anything, so restarting the dev server is a no-op once | ||
| 29 | + # the seed has been applied. Production deployments leave this | ||
| 30 | + # property unset and the @ConditionalOnProperty bean never loads. | ||
| 31 | + seed: true | ||
| 24 | 32 | ||
| 25 | logging: | 33 | logging: |
| 26 | level: | 34 | level: |
gradle.properties
| @@ -19,6 +19,6 @@ kotlin.incremental=true | @@ -19,6 +19,6 @@ kotlin.incremental=true | ||
| 19 | # api/api-v1/build.gradle.kts. | 19 | # api/api-v1/build.gradle.kts. |
| 20 | # Bump this line in lockstep with PROGRESS.md's "Latest version" | 20 | # Bump this line in lockstep with PROGRESS.md's "Latest version" |
| 21 | # row when shipping a new working surface. | 21 | # row when shipping a new working surface. |
| 22 | -vibeerp.version=0.28.0-SNAPSHOT | 22 | +vibeerp.version=0.29.0-SNAPSHOT |
| 23 | vibeerp.api.version=1.0.0-SNAPSHOT | 23 | vibeerp.api.version=1.0.0-SNAPSHOT |
| 24 | vibeerp.group=org.vibeerp | 24 | vibeerp.group=org.vibeerp |
platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/SpaController.kt
0 → 100644
| 1 | +package org.vibeerp.platform.bootstrap.web | ||
| 2 | + | ||
| 3 | +import org.springframework.stereotype.Controller | ||
| 4 | +import org.springframework.web.bind.annotation.GetMapping | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * Static SPA fallback controller. | ||
| 8 | + * | ||
| 9 | + * **The problem.** The vibe_erp Web SPA (the `:web` Gradle subproject) | ||
| 10 | + * uses React Router's HTML5 history mode for client-side routing. | ||
| 11 | + * When a user hits `http://host/sales-orders` directly — by typing | ||
| 12 | + * the URL, refreshing the page, or clicking a deep link — Spring | ||
| 13 | + * Boot's static resource handler tries to serve a file at | ||
| 14 | + * `static/sales-orders` and returns 404 because no such file | ||
| 15 | + * exists. The actual routing for `/sales-orders` lives inside the | ||
| 16 | + * SPA's compiled JavaScript, so we need the server to return | ||
| 17 | + * `static/index.html` for every SPA route and let React Router | ||
| 18 | + * take over once the bundle has loaded. | ||
| 19 | + * | ||
| 20 | + * **Why an explicit list, not a catch-all.** Spring Security needs | ||
| 21 | + * to permit the same paths the SPA uses, and a catch-all | ||
| 22 | + * `@GetMapping` with a wildcard path matcher would also intercept | ||
| 23 | + * legitimate 404s for typoed API URLs under the api-v1 prefix | ||
| 24 | + * (turning them into "here's the SPA's 404 page" instead of an | ||
| 25 | + * honest server-side 404). Listing the SPA's known top-level routes | ||
| 26 | + * here keeps the seam explicit: adding a new sidebar item to | ||
| 27 | + * AppLayout.tsx requires adding the route here. The list is small | ||
| 28 | + * (12 entries) so the maintenance cost is trivial; if it ever | ||
| 29 | + * passes ~25 entries we can revisit with a single regex matcher. | ||
| 30 | + * | ||
| 31 | + * **Forward, not redirect.** `forward:/index.html` is an internal | ||
| 32 | + * RequestDispatcher.forward — the URL bar still shows | ||
| 33 | + * `/sales-orders` and Spring Security still sees the original | ||
| 34 | + * request path. Redirecting (`redirect:/index.html`) would lose | ||
| 35 | + * the deep link entirely and break the back button. | ||
| 36 | + * | ||
| 37 | + * **The "/" entry.** Without it, hitting the root would also fall | ||
| 38 | + * through to the static resource handler, which DOES correctly | ||
| 39 | + * serve `static/index.html` for `/`. We list it explicitly anyway | ||
| 40 | + * so the route table is self-explanatory. | ||
| 41 | + * | ||
| 42 | + * **API paths are NOT in this list.** Anything under the api-v1 | ||
| 43 | + * prefix matches a `@RestController` mapping registered higher up | ||
| 44 | + * in the request-mapping chain, so the SPA fallback is never | ||
| 45 | + * reached for an authentic API request. For an UNKNOWN API path, | ||
| 46 | + * the absence of a controller match returns a real 404, which is | ||
| 47 | + * what we want. | ||
| 48 | + * | ||
| 49 | + * **Where this controller is in the build.** Lives in | ||
| 50 | + * `platform-bootstrap` rather than `:distribution` because it is | ||
| 51 | + * a `@Controller` that needs to be picked up by Spring's component | ||
| 52 | + * scan, and `platform-bootstrap` is the only module with the | ||
| 53 | + * `@SpringBootApplication`. Putting it in `:distribution` would | ||
| 54 | + * require adding a Spring scan to a module that has zero Kotlin | ||
| 55 | + * sources of its own. | ||
| 56 | + */ | ||
| 57 | +@Controller | ||
| 58 | +class SpaController { | ||
| 59 | + | ||
| 60 | + @GetMapping( | ||
| 61 | + value = [ | ||
| 62 | + // Root + auth | ||
| 63 | + "/", | ||
| 64 | + "/login", | ||
| 65 | + | ||
| 66 | + // Catalog & partners | ||
| 67 | + "/items", "/items/", "/items/**", | ||
| 68 | + "/uoms", "/uoms/", "/uoms/**", | ||
| 69 | + "/partners", "/partners/", "/partners/**", | ||
| 70 | + | ||
| 71 | + // Inventory | ||
| 72 | + "/locations", "/locations/", "/locations/**", | ||
| 73 | + "/balances", "/balances/", "/balances/**", | ||
| 74 | + "/movements", "/movements/", "/movements/**", | ||
| 75 | + | ||
| 76 | + // Orders | ||
| 77 | + "/sales-orders", "/sales-orders/", "/sales-orders/**", | ||
| 78 | + "/purchase-orders", "/purchase-orders/", "/purchase-orders/**", | ||
| 79 | + | ||
| 80 | + // Production | ||
| 81 | + "/work-orders", "/work-orders/", "/work-orders/**", | ||
| 82 | + "/shop-floor", "/shop-floor/", "/shop-floor/**", | ||
| 83 | + | ||
| 84 | + // Finance | ||
| 85 | + "/journal-entries", "/journal-entries/", "/journal-entries/**", | ||
| 86 | + ], | ||
| 87 | + ) | ||
| 88 | + fun spa(): String = "forward:/index.html" | ||
| 89 | +} |
platform/platform-security/src/main/kotlin/org/vibeerp/platform/security/SecurityConfiguration.kt
| @@ -37,6 +37,15 @@ class SecurityConfiguration { | @@ -37,6 +37,15 @@ class SecurityConfiguration { | ||
| 37 | .cors { } // default; tighten in v0.4 once the React SPA exists | 37 | .cors { } // default; tighten in v0.4 once the React SPA exists |
| 38 | .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } | 38 | .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } |
| 39 | .authorizeHttpRequests { auth -> | 39 | .authorizeHttpRequests { auth -> |
| 40 | + // ─── Public API endpoints ─────────────────────────── | ||
| 41 | + // | ||
| 42 | + // The framework's API surface is split into a tiny | ||
| 43 | + // public allowlist and a much larger authenticated | ||
| 44 | + // surface. Listing the public bits explicitly first | ||
| 45 | + // (and authenticating /api/** as a whole afterwards) | ||
| 46 | + // keeps the matcher precedence honest: a typo in a | ||
| 47 | + // public path can never accidentally permit an | ||
| 48 | + // authenticated endpoint. | ||
| 40 | auth.requestMatchers( | 49 | auth.requestMatchers( |
| 41 | // Public actuator probes | 50 | // Public actuator probes |
| 42 | "/actuator/health", | 51 | "/actuator/health", |
| @@ -46,17 +55,65 @@ class SecurityConfiguration { | @@ -46,17 +55,65 @@ class SecurityConfiguration { | ||
| 46 | // Auth endpoints themselves cannot require auth | 55 | // Auth endpoints themselves cannot require auth |
| 47 | "/api/v1/auth/login", | 56 | "/api/v1/auth/login", |
| 48 | "/api/v1/auth/refresh", | 57 | "/api/v1/auth/refresh", |
| 49 | - // OpenAPI / Swagger UI — public because it describes the | ||
| 50 | - // API surface, not the data. The *data* still requires a | ||
| 51 | - // valid JWT: an unauthenticated "Try it out" call from | ||
| 52 | - // the Swagger UI against a pbc endpoint returns 401 just | ||
| 53 | - // like a curl does. Exposing the spec is the first step | ||
| 54 | - // toward both the future R1 web SPA (OpenAPI codegen) | ||
| 55 | - // and the A1 MCP server (discoverable tool catalog). | 58 | + // OpenAPI / Swagger UI — public because it describes |
| 59 | + // the API surface, not the data. The *data* still | ||
| 60 | + // requires a valid JWT: an unauthenticated "Try it | ||
| 61 | + // out" call against a pbc endpoint returns 401. | ||
| 56 | "/v3/api-docs/**", | 62 | "/v3/api-docs/**", |
| 57 | "/swagger-ui/**", | 63 | "/swagger-ui/**", |
| 58 | "/swagger-ui.html", | 64 | "/swagger-ui.html", |
| 59 | ).permitAll() | 65 | ).permitAll() |
| 66 | + | ||
| 67 | + // Every other /api/** call requires a Bearer JWT. | ||
| 68 | + // This MUST come before the SPA permitAll below; the | ||
| 69 | + // permit-all rule for "/" + static assets uses | ||
| 70 | + // anyRequest() and would otherwise swallow API calls | ||
| 71 | + // too. The order preserves the framework's "API is | ||
| 72 | + // always authenticated" invariant even as the SPA | ||
| 73 | + // moves into the same fat-jar. | ||
| 74 | + auth.requestMatchers("/api/**").authenticated() | ||
| 75 | + | ||
| 76 | + // ─── Web SPA (the :web React+TS bundle) ───────────── | ||
| 77 | + // | ||
| 78 | + // Static asset paths the Vite build produces. | ||
| 79 | + // Permitted because the SPA itself is just HTML+CSS+JS | ||
| 80 | + // — every secret is fetched from /api/** which is | ||
| 81 | + // already gated above. A user without a valid JWT | ||
| 82 | + // sees the login page instead of a forbidden screen, | ||
| 83 | + // which is the correct UX. | ||
| 84 | + auth.requestMatchers( | ||
| 85 | + "/", | ||
| 86 | + "/index.html", | ||
| 87 | + "/favicon.svg", | ||
| 88 | + "/favicon.ico", | ||
| 89 | + "/manifest.json", | ||
| 90 | + "/assets/**", | ||
| 91 | + "/static/**", | ||
| 92 | + ).permitAll() | ||
| 93 | + | ||
| 94 | + // SPA route paths handled by the SpaController in | ||
| 95 | + // platform-bootstrap (which forwards them to | ||
| 96 | + // /index.html so React Router can take over). The | ||
| 97 | + // controller and this list MUST stay in sync — adding | ||
| 98 | + // a sidebar entry to AppLayout.tsx implies updating | ||
| 99 | + // both. See SpaController for the rationale. | ||
| 100 | + auth.requestMatchers( | ||
| 101 | + "/login", | ||
| 102 | + "/items", "/items/**", | ||
| 103 | + "/uoms", "/uoms/**", | ||
| 104 | + "/partners", "/partners/**", | ||
| 105 | + "/locations", "/locations/**", | ||
| 106 | + "/balances", "/balances/**", | ||
| 107 | + "/movements", "/movements/**", | ||
| 108 | + "/sales-orders", "/sales-orders/**", | ||
| 109 | + "/purchase-orders", "/purchase-orders/**", | ||
| 110 | + "/work-orders", "/work-orders/**", | ||
| 111 | + "/shop-floor", "/shop-floor/**", | ||
| 112 | + "/journal-entries", "/journal-entries/**", | ||
| 113 | + ).permitAll() | ||
| 114 | + | ||
| 115 | + // Anything else — return 401 so a typoed deep link | ||
| 116 | + // doesn't accidentally serve the SPA shell. | ||
| 60 | auth.anyRequest().authenticated() | 117 | auth.anyRequest().authenticated() |
| 61 | } | 118 | } |
| 62 | .oauth2ResourceServer { rs -> | 119 | .oauth2ResourceServer { rs -> |
settings.gradle.kts
| @@ -89,6 +89,16 @@ project(":pbc:pbc-production").projectDir = file("pbc/pbc-production") | @@ -89,6 +89,16 @@ project(":pbc:pbc-production").projectDir = file("pbc/pbc-production") | ||
| 89 | include(":reference-customer:plugin-printing-shop") | 89 | include(":reference-customer:plugin-printing-shop") |
| 90 | project(":reference-customer:plugin-printing-shop").projectDir = file("reference-customer/plugin-printing-shop") | 90 | project(":reference-customer:plugin-printing-shop").projectDir = file("reference-customer/plugin-printing-shop") |
| 91 | 91 | ||
| 92 | +// ─── Web SPA ──────────────────────────────────────────────────────── | ||
| 93 | +// | ||
| 94 | +// `:web` is a Gradle wrapper around an npm build. It produces a | ||
| 95 | +// `dist/` directory of static assets that `:distribution` consumes | ||
| 96 | +// at processResources time. See `web/build.gradle.kts` for the | ||
| 97 | +// rationale (no Kotlin/JVM source set, no node-gradle plugin — | ||
| 98 | +// just Exec tasks against system npm). | ||
| 99 | +include(":web") | ||
| 100 | +project(":web").projectDir = file("web") | ||
| 101 | + | ||
| 92 | // ─── Distribution (assembles the runnable image) ──────────────────── | 102 | // ─── Distribution (assembles the runnable image) ──────────────────── |
| 93 | include(":distribution") | 103 | include(":distribution") |
| 94 | project(":distribution").projectDir = file("distribution") | 104 | project(":distribution").projectDir = file("distribution") |
web/.gitignore
0 → 100644
web/build.gradle.kts
0 → 100644
| 1 | +// vibe_erp web SPA — Gradle wrapper around the npm build. | ||
| 2 | +// | ||
| 3 | +// **Why no Kotlin/JVM source set.** This subproject has zero JVM | ||
| 4 | +// code: every artifact under src/ is TypeScript/React. The Gradle | ||
| 5 | +// presence is purely so `./gradlew build` runs the npm build as | ||
| 6 | +// part of the standard build pipeline, and so :distribution can | ||
| 7 | +// declare a normal project dependency on the produced static | ||
| 8 | +// bundle instead of a fragile path reference. | ||
| 9 | +// | ||
| 10 | +// **Tasks.** | ||
| 11 | +// - `npmInstall` → runs `npm install` in web/. Inputs: | ||
| 12 | +// package.json + package-lock.json. Output: node_modules/. | ||
| 13 | +// - `npmBuild` → runs `npm run build` in web/. Inputs: | ||
| 14 | +// src/**, index.html, vite.config.ts, tsconfig*.json, | ||
| 15 | +// tailwind.config.js, postcss.config.js. Output: dist/. | ||
| 16 | +// - `assemble` → wired to depend on npmBuild so a normal | ||
| 17 | +// `./gradlew :web:build` produces dist/. | ||
| 18 | +// | ||
| 19 | +// **No npm/node Gradle plugin.** I deliberately avoided | ||
| 20 | +// `com.github.node-gradle.node` to keep the dep surface minimal — | ||
| 21 | +// every plugin is one more thing to keep current with Gradle and | ||
| 22 | +// JDK upgrades. Exec tasks are stable and the inputs/outputs | ||
| 23 | +// declarations give Gradle the same up-to-date checking the plugin | ||
| 24 | +// would. | ||
| 25 | +// | ||
| 26 | +// **Where the bundle goes.** Read by `:distribution` as a normal | ||
| 27 | +// project artifact via the `webStaticBundle` configuration below; | ||
| 28 | +// the consuming module copies dist/ into the Spring Boot | ||
| 29 | +// classpath under `static/` at processResources time so the | ||
| 30 | +// bootJar contains the entire SPA. | ||
| 31 | + | ||
| 32 | +import org.gradle.api.tasks.Exec | ||
| 33 | + | ||
| 34 | +plugins { | ||
| 35 | + base | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +description = "vibe_erp web SPA — Vite + React + TS, served by Spring Boot at the root path." | ||
| 39 | + | ||
| 40 | +// Resolve npm against the system PATH. Same approach as the | ||
| 41 | +// reference plug-in's installToDev task — keeps developer setup | ||
| 42 | +// simple ("you must have node + npm on PATH"). | ||
| 43 | +val npmCommand: String = if (org.gradle.internal.os.OperatingSystem.current().isWindows) "npm.cmd" else "npm" | ||
| 44 | + | ||
| 45 | +val npmInstall by tasks.registering(Exec::class) { | ||
| 46 | + group = "build" | ||
| 47 | + description = "Run `npm install` in web/." | ||
| 48 | + workingDir = projectDir | ||
| 49 | + commandLine(npmCommand, "install", "--no-audit", "--no-fund", "--silent") | ||
| 50 | + | ||
| 51 | + inputs.file("package.json") | ||
| 52 | + inputs.file("package-lock.json").optional() | ||
| 53 | + outputs.dir("node_modules") | ||
| 54 | +} | ||
| 55 | + | ||
| 56 | +val npmBuild by tasks.registering(Exec::class) { | ||
| 57 | + group = "build" | ||
| 58 | + description = "Run `npm run build` in web/. Produces dist/." | ||
| 59 | + workingDir = projectDir | ||
| 60 | + commandLine(npmCommand, "run", "build") | ||
| 61 | + | ||
| 62 | + dependsOn(npmInstall) | ||
| 63 | + | ||
| 64 | + inputs.dir("src") | ||
| 65 | + inputs.file("index.html") | ||
| 66 | + inputs.file("vite.config.ts") | ||
| 67 | + inputs.file("tsconfig.json") | ||
| 68 | + inputs.file("tsconfig.node.json") | ||
| 69 | + inputs.file("tailwind.config.js") | ||
| 70 | + inputs.file("postcss.config.js") | ||
| 71 | + inputs.file("package.json") | ||
| 72 | + | ||
| 73 | + outputs.dir("dist") | ||
| 74 | +} | ||
| 75 | + | ||
| 76 | +tasks.named("assemble") { | ||
| 77 | + dependsOn(npmBuild) | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +// Outgoing configuration so :distribution can consume the dist | ||
| 81 | +// directory as a normal Gradle artifact. Using a single-element | ||
| 82 | +// `artifacts` block keeps the consumer side trivial. | ||
| 83 | +val webStaticBundle: Configuration by configurations.creating { | ||
| 84 | + isCanBeConsumed = true | ||
| 85 | + isCanBeResolved = false | ||
| 86 | +} | ||
| 87 | + | ||
| 88 | +artifacts { | ||
| 89 | + add(webStaticBundle.name, file("dist")) { | ||
| 90 | + builtBy(npmBuild) | ||
| 91 | + } | ||
| 92 | +} |
web/index.html
0 → 100644
| 1 | +<!doctype html> | ||
| 2 | +<html lang="en"> | ||
| 3 | + <head> | ||
| 4 | + <meta charset="UTF-8" /> | ||
| 5 | + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||
| 6 | + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| 7 | + <meta name="description" content="vibe_erp — composable ERP framework for the printing industry" /> | ||
| 8 | + <title>vibe_erp</title> | ||
| 9 | + </head> | ||
| 10 | + <body class="bg-slate-50"> | ||
| 11 | + <div id="root"></div> | ||
| 12 | + <script type="module" src="/src/main.tsx"></script> | ||
| 13 | + </body> | ||
| 14 | +</html> |
web/package-lock.json
0 → 100644
| 1 | +{ | ||
| 2 | + "name": "vibe-erp-web", | ||
| 3 | + "version": "0.0.0", | ||
| 4 | + "lockfileVersion": 3, | ||
| 5 | + "requires": true, | ||
| 6 | + "packages": { | ||
| 7 | + "": { | ||
| 8 | + "name": "vibe-erp-web", | ||
| 9 | + "version": "0.0.0", | ||
| 10 | + "dependencies": { | ||
| 11 | + "react": "^18.3.1", | ||
| 12 | + "react-dom": "^18.3.1", | ||
| 13 | + "react-router-dom": "^6.28.0" | ||
| 14 | + }, | ||
| 15 | + "devDependencies": { | ||
| 16 | + "@types/node": "^25.5.2", | ||
| 17 | + "@types/react": "^18.3.12", | ||
| 18 | + "@types/react-dom": "^18.3.1", | ||
| 19 | + "@vitejs/plugin-react": "^4.3.3", | ||
| 20 | + "autoprefixer": "^10.4.20", | ||
| 21 | + "postcss": "^8.4.49", | ||
| 22 | + "tailwindcss": "^3.4.15", | ||
| 23 | + "typescript": "^5.6.3", | ||
| 24 | + "vite": "^5.4.11" | ||
| 25 | + } | ||
| 26 | + }, | ||
| 27 | + "node_modules/@alloc/quick-lru": { | ||
| 28 | + "version": "5.2.0", | ||
| 29 | + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", | ||
| 30 | + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", | ||
| 31 | + "dev": true, | ||
| 32 | + "license": "MIT", | ||
| 33 | + "engines": { | ||
| 34 | + "node": ">=10" | ||
| 35 | + }, | ||
| 36 | + "funding": { | ||
| 37 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 38 | + } | ||
| 39 | + }, | ||
| 40 | + "node_modules/@babel/code-frame": { | ||
| 41 | + "version": "7.29.0", | ||
| 42 | + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", | ||
| 43 | + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", | ||
| 44 | + "dev": true, | ||
| 45 | + "license": "MIT", | ||
| 46 | + "dependencies": { | ||
| 47 | + "@babel/helper-validator-identifier": "^7.28.5", | ||
| 48 | + "js-tokens": "^4.0.0", | ||
| 49 | + "picocolors": "^1.1.1" | ||
| 50 | + }, | ||
| 51 | + "engines": { | ||
| 52 | + "node": ">=6.9.0" | ||
| 53 | + } | ||
| 54 | + }, | ||
| 55 | + "node_modules/@babel/compat-data": { | ||
| 56 | + "version": "7.29.0", | ||
| 57 | + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", | ||
| 58 | + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", | ||
| 59 | + "dev": true, | ||
| 60 | + "license": "MIT", | ||
| 61 | + "engines": { | ||
| 62 | + "node": ">=6.9.0" | ||
| 63 | + } | ||
| 64 | + }, | ||
| 65 | + "node_modules/@babel/core": { | ||
| 66 | + "version": "7.29.0", | ||
| 67 | + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", | ||
| 68 | + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", | ||
| 69 | + "dev": true, | ||
| 70 | + "license": "MIT", | ||
| 71 | + "dependencies": { | ||
| 72 | + "@babel/code-frame": "^7.29.0", | ||
| 73 | + "@babel/generator": "^7.29.0", | ||
| 74 | + "@babel/helper-compilation-targets": "^7.28.6", | ||
| 75 | + "@babel/helper-module-transforms": "^7.28.6", | ||
| 76 | + "@babel/helpers": "^7.28.6", | ||
| 77 | + "@babel/parser": "^7.29.0", | ||
| 78 | + "@babel/template": "^7.28.6", | ||
| 79 | + "@babel/traverse": "^7.29.0", | ||
| 80 | + "@babel/types": "^7.29.0", | ||
| 81 | + "@jridgewell/remapping": "^2.3.5", | ||
| 82 | + "convert-source-map": "^2.0.0", | ||
| 83 | + "debug": "^4.1.0", | ||
| 84 | + "gensync": "^1.0.0-beta.2", | ||
| 85 | + "json5": "^2.2.3", | ||
| 86 | + "semver": "^6.3.1" | ||
| 87 | + }, | ||
| 88 | + "engines": { | ||
| 89 | + "node": ">=6.9.0" | ||
| 90 | + }, | ||
| 91 | + "funding": { | ||
| 92 | + "type": "opencollective", | ||
| 93 | + "url": "https://opencollective.com/babel" | ||
| 94 | + } | ||
| 95 | + }, | ||
| 96 | + "node_modules/@babel/generator": { | ||
| 97 | + "version": "7.29.1", | ||
| 98 | + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", | ||
| 99 | + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", | ||
| 100 | + "dev": true, | ||
| 101 | + "license": "MIT", | ||
| 102 | + "dependencies": { | ||
| 103 | + "@babel/parser": "^7.29.0", | ||
| 104 | + "@babel/types": "^7.29.0", | ||
| 105 | + "@jridgewell/gen-mapping": "^0.3.12", | ||
| 106 | + "@jridgewell/trace-mapping": "^0.3.28", | ||
| 107 | + "jsesc": "^3.0.2" | ||
| 108 | + }, | ||
| 109 | + "engines": { | ||
| 110 | + "node": ">=6.9.0" | ||
| 111 | + } | ||
| 112 | + }, | ||
| 113 | + "node_modules/@babel/helper-compilation-targets": { | ||
| 114 | + "version": "7.28.6", | ||
| 115 | + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", | ||
| 116 | + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", | ||
| 117 | + "dev": true, | ||
| 118 | + "license": "MIT", | ||
| 119 | + "dependencies": { | ||
| 120 | + "@babel/compat-data": "^7.28.6", | ||
| 121 | + "@babel/helper-validator-option": "^7.27.1", | ||
| 122 | + "browserslist": "^4.24.0", | ||
| 123 | + "lru-cache": "^5.1.1", | ||
| 124 | + "semver": "^6.3.1" | ||
| 125 | + }, | ||
| 126 | + "engines": { | ||
| 127 | + "node": ">=6.9.0" | ||
| 128 | + } | ||
| 129 | + }, | ||
| 130 | + "node_modules/@babel/helper-globals": { | ||
| 131 | + "version": "7.28.0", | ||
| 132 | + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", | ||
| 133 | + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", | ||
| 134 | + "dev": true, | ||
| 135 | + "license": "MIT", | ||
| 136 | + "engines": { | ||
| 137 | + "node": ">=6.9.0" | ||
| 138 | + } | ||
| 139 | + }, | ||
| 140 | + "node_modules/@babel/helper-module-imports": { | ||
| 141 | + "version": "7.28.6", | ||
| 142 | + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", | ||
| 143 | + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", | ||
| 144 | + "dev": true, | ||
| 145 | + "license": "MIT", | ||
| 146 | + "dependencies": { | ||
| 147 | + "@babel/traverse": "^7.28.6", | ||
| 148 | + "@babel/types": "^7.28.6" | ||
| 149 | + }, | ||
| 150 | + "engines": { | ||
| 151 | + "node": ">=6.9.0" | ||
| 152 | + } | ||
| 153 | + }, | ||
| 154 | + "node_modules/@babel/helper-module-transforms": { | ||
| 155 | + "version": "7.28.6", | ||
| 156 | + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", | ||
| 157 | + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", | ||
| 158 | + "dev": true, | ||
| 159 | + "license": "MIT", | ||
| 160 | + "dependencies": { | ||
| 161 | + "@babel/helper-module-imports": "^7.28.6", | ||
| 162 | + "@babel/helper-validator-identifier": "^7.28.5", | ||
| 163 | + "@babel/traverse": "^7.28.6" | ||
| 164 | + }, | ||
| 165 | + "engines": { | ||
| 166 | + "node": ">=6.9.0" | ||
| 167 | + }, | ||
| 168 | + "peerDependencies": { | ||
| 169 | + "@babel/core": "^7.0.0" | ||
| 170 | + } | ||
| 171 | + }, | ||
| 172 | + "node_modules/@babel/helper-plugin-utils": { | ||
| 173 | + "version": "7.28.6", | ||
| 174 | + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", | ||
| 175 | + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", | ||
| 176 | + "dev": true, | ||
| 177 | + "license": "MIT", | ||
| 178 | + "engines": { | ||
| 179 | + "node": ">=6.9.0" | ||
| 180 | + } | ||
| 181 | + }, | ||
| 182 | + "node_modules/@babel/helper-string-parser": { | ||
| 183 | + "version": "7.27.1", | ||
| 184 | + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", | ||
| 185 | + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", | ||
| 186 | + "dev": true, | ||
| 187 | + "license": "MIT", | ||
| 188 | + "engines": { | ||
| 189 | + "node": ">=6.9.0" | ||
| 190 | + } | ||
| 191 | + }, | ||
| 192 | + "node_modules/@babel/helper-validator-identifier": { | ||
| 193 | + "version": "7.28.5", | ||
| 194 | + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", | ||
| 195 | + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", | ||
| 196 | + "dev": true, | ||
| 197 | + "license": "MIT", | ||
| 198 | + "engines": { | ||
| 199 | + "node": ">=6.9.0" | ||
| 200 | + } | ||
| 201 | + }, | ||
| 202 | + "node_modules/@babel/helper-validator-option": { | ||
| 203 | + "version": "7.27.1", | ||
| 204 | + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", | ||
| 205 | + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", | ||
| 206 | + "dev": true, | ||
| 207 | + "license": "MIT", | ||
| 208 | + "engines": { | ||
| 209 | + "node": ">=6.9.0" | ||
| 210 | + } | ||
| 211 | + }, | ||
| 212 | + "node_modules/@babel/helpers": { | ||
| 213 | + "version": "7.29.2", | ||
| 214 | + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", | ||
| 215 | + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", | ||
| 216 | + "dev": true, | ||
| 217 | + "license": "MIT", | ||
| 218 | + "dependencies": { | ||
| 219 | + "@babel/template": "^7.28.6", | ||
| 220 | + "@babel/types": "^7.29.0" | ||
| 221 | + }, | ||
| 222 | + "engines": { | ||
| 223 | + "node": ">=6.9.0" | ||
| 224 | + } | ||
| 225 | + }, | ||
| 226 | + "node_modules/@babel/parser": { | ||
| 227 | + "version": "7.29.2", | ||
| 228 | + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", | ||
| 229 | + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", | ||
| 230 | + "dev": true, | ||
| 231 | + "license": "MIT", | ||
| 232 | + "dependencies": { | ||
| 233 | + "@babel/types": "^7.29.0" | ||
| 234 | + }, | ||
| 235 | + "bin": { | ||
| 236 | + "parser": "bin/babel-parser.js" | ||
| 237 | + }, | ||
| 238 | + "engines": { | ||
| 239 | + "node": ">=6.0.0" | ||
| 240 | + } | ||
| 241 | + }, | ||
| 242 | + "node_modules/@babel/plugin-transform-react-jsx-self": { | ||
| 243 | + "version": "7.27.1", | ||
| 244 | + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", | ||
| 245 | + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", | ||
| 246 | + "dev": true, | ||
| 247 | + "license": "MIT", | ||
| 248 | + "dependencies": { | ||
| 249 | + "@babel/helper-plugin-utils": "^7.27.1" | ||
| 250 | + }, | ||
| 251 | + "engines": { | ||
| 252 | + "node": ">=6.9.0" | ||
| 253 | + }, | ||
| 254 | + "peerDependencies": { | ||
| 255 | + "@babel/core": "^7.0.0-0" | ||
| 256 | + } | ||
| 257 | + }, | ||
| 258 | + "node_modules/@babel/plugin-transform-react-jsx-source": { | ||
| 259 | + "version": "7.27.1", | ||
| 260 | + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", | ||
| 261 | + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", | ||
| 262 | + "dev": true, | ||
| 263 | + "license": "MIT", | ||
| 264 | + "dependencies": { | ||
| 265 | + "@babel/helper-plugin-utils": "^7.27.1" | ||
| 266 | + }, | ||
| 267 | + "engines": { | ||
| 268 | + "node": ">=6.9.0" | ||
| 269 | + }, | ||
| 270 | + "peerDependencies": { | ||
| 271 | + "@babel/core": "^7.0.0-0" | ||
| 272 | + } | ||
| 273 | + }, | ||
| 274 | + "node_modules/@babel/template": { | ||
| 275 | + "version": "7.28.6", | ||
| 276 | + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", | ||
| 277 | + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", | ||
| 278 | + "dev": true, | ||
| 279 | + "license": "MIT", | ||
| 280 | + "dependencies": { | ||
| 281 | + "@babel/code-frame": "^7.28.6", | ||
| 282 | + "@babel/parser": "^7.28.6", | ||
| 283 | + "@babel/types": "^7.28.6" | ||
| 284 | + }, | ||
| 285 | + "engines": { | ||
| 286 | + "node": ">=6.9.0" | ||
| 287 | + } | ||
| 288 | + }, | ||
| 289 | + "node_modules/@babel/traverse": { | ||
| 290 | + "version": "7.29.0", | ||
| 291 | + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", | ||
| 292 | + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", | ||
| 293 | + "dev": true, | ||
| 294 | + "license": "MIT", | ||
| 295 | + "dependencies": { | ||
| 296 | + "@babel/code-frame": "^7.29.0", | ||
| 297 | + "@babel/generator": "^7.29.0", | ||
| 298 | + "@babel/helper-globals": "^7.28.0", | ||
| 299 | + "@babel/parser": "^7.29.0", | ||
| 300 | + "@babel/template": "^7.28.6", | ||
| 301 | + "@babel/types": "^7.29.0", | ||
| 302 | + "debug": "^4.3.1" | ||
| 303 | + }, | ||
| 304 | + "engines": { | ||
| 305 | + "node": ">=6.9.0" | ||
| 306 | + } | ||
| 307 | + }, | ||
| 308 | + "node_modules/@babel/types": { | ||
| 309 | + "version": "7.29.0", | ||
| 310 | + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", | ||
| 311 | + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", | ||
| 312 | + "dev": true, | ||
| 313 | + "license": "MIT", | ||
| 314 | + "dependencies": { | ||
| 315 | + "@babel/helper-string-parser": "^7.27.1", | ||
| 316 | + "@babel/helper-validator-identifier": "^7.28.5" | ||
| 317 | + }, | ||
| 318 | + "engines": { | ||
| 319 | + "node": ">=6.9.0" | ||
| 320 | + } | ||
| 321 | + }, | ||
| 322 | + "node_modules/@esbuild/aix-ppc64": { | ||
| 323 | + "version": "0.21.5", | ||
| 324 | + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", | ||
| 325 | + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", | ||
| 326 | + "cpu": [ | ||
| 327 | + "ppc64" | ||
| 328 | + ], | ||
| 329 | + "dev": true, | ||
| 330 | + "license": "MIT", | ||
| 331 | + "optional": true, | ||
| 332 | + "os": [ | ||
| 333 | + "aix" | ||
| 334 | + ], | ||
| 335 | + "engines": { | ||
| 336 | + "node": ">=12" | ||
| 337 | + } | ||
| 338 | + }, | ||
| 339 | + "node_modules/@esbuild/android-arm": { | ||
| 340 | + "version": "0.21.5", | ||
| 341 | + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", | ||
| 342 | + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", | ||
| 343 | + "cpu": [ | ||
| 344 | + "arm" | ||
| 345 | + ], | ||
| 346 | + "dev": true, | ||
| 347 | + "license": "MIT", | ||
| 348 | + "optional": true, | ||
| 349 | + "os": [ | ||
| 350 | + "android" | ||
| 351 | + ], | ||
| 352 | + "engines": { | ||
| 353 | + "node": ">=12" | ||
| 354 | + } | ||
| 355 | + }, | ||
| 356 | + "node_modules/@esbuild/android-arm64": { | ||
| 357 | + "version": "0.21.5", | ||
| 358 | + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", | ||
| 359 | + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", | ||
| 360 | + "cpu": [ | ||
| 361 | + "arm64" | ||
| 362 | + ], | ||
| 363 | + "dev": true, | ||
| 364 | + "license": "MIT", | ||
| 365 | + "optional": true, | ||
| 366 | + "os": [ | ||
| 367 | + "android" | ||
| 368 | + ], | ||
| 369 | + "engines": { | ||
| 370 | + "node": ">=12" | ||
| 371 | + } | ||
| 372 | + }, | ||
| 373 | + "node_modules/@esbuild/android-x64": { | ||
| 374 | + "version": "0.21.5", | ||
| 375 | + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", | ||
| 376 | + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", | ||
| 377 | + "cpu": [ | ||
| 378 | + "x64" | ||
| 379 | + ], | ||
| 380 | + "dev": true, | ||
| 381 | + "license": "MIT", | ||
| 382 | + "optional": true, | ||
| 383 | + "os": [ | ||
| 384 | + "android" | ||
| 385 | + ], | ||
| 386 | + "engines": { | ||
| 387 | + "node": ">=12" | ||
| 388 | + } | ||
| 389 | + }, | ||
| 390 | + "node_modules/@esbuild/darwin-arm64": { | ||
| 391 | + "version": "0.21.5", | ||
| 392 | + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", | ||
| 393 | + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", | ||
| 394 | + "cpu": [ | ||
| 395 | + "arm64" | ||
| 396 | + ], | ||
| 397 | + "dev": true, | ||
| 398 | + "license": "MIT", | ||
| 399 | + "optional": true, | ||
| 400 | + "os": [ | ||
| 401 | + "darwin" | ||
| 402 | + ], | ||
| 403 | + "engines": { | ||
| 404 | + "node": ">=12" | ||
| 405 | + } | ||
| 406 | + }, | ||
| 407 | + "node_modules/@esbuild/darwin-x64": { | ||
| 408 | + "version": "0.21.5", | ||
| 409 | + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", | ||
| 410 | + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", | ||
| 411 | + "cpu": [ | ||
| 412 | + "x64" | ||
| 413 | + ], | ||
| 414 | + "dev": true, | ||
| 415 | + "license": "MIT", | ||
| 416 | + "optional": true, | ||
| 417 | + "os": [ | ||
| 418 | + "darwin" | ||
| 419 | + ], | ||
| 420 | + "engines": { | ||
| 421 | + "node": ">=12" | ||
| 422 | + } | ||
| 423 | + }, | ||
| 424 | + "node_modules/@esbuild/freebsd-arm64": { | ||
| 425 | + "version": "0.21.5", | ||
| 426 | + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", | ||
| 427 | + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", | ||
| 428 | + "cpu": [ | ||
| 429 | + "arm64" | ||
| 430 | + ], | ||
| 431 | + "dev": true, | ||
| 432 | + "license": "MIT", | ||
| 433 | + "optional": true, | ||
| 434 | + "os": [ | ||
| 435 | + "freebsd" | ||
| 436 | + ], | ||
| 437 | + "engines": { | ||
| 438 | + "node": ">=12" | ||
| 439 | + } | ||
| 440 | + }, | ||
| 441 | + "node_modules/@esbuild/freebsd-x64": { | ||
| 442 | + "version": "0.21.5", | ||
| 443 | + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", | ||
| 444 | + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", | ||
| 445 | + "cpu": [ | ||
| 446 | + "x64" | ||
| 447 | + ], | ||
| 448 | + "dev": true, | ||
| 449 | + "license": "MIT", | ||
| 450 | + "optional": true, | ||
| 451 | + "os": [ | ||
| 452 | + "freebsd" | ||
| 453 | + ], | ||
| 454 | + "engines": { | ||
| 455 | + "node": ">=12" | ||
| 456 | + } | ||
| 457 | + }, | ||
| 458 | + "node_modules/@esbuild/linux-arm": { | ||
| 459 | + "version": "0.21.5", | ||
| 460 | + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", | ||
| 461 | + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", | ||
| 462 | + "cpu": [ | ||
| 463 | + "arm" | ||
| 464 | + ], | ||
| 465 | + "dev": true, | ||
| 466 | + "license": "MIT", | ||
| 467 | + "optional": true, | ||
| 468 | + "os": [ | ||
| 469 | + "linux" | ||
| 470 | + ], | ||
| 471 | + "engines": { | ||
| 472 | + "node": ">=12" | ||
| 473 | + } | ||
| 474 | + }, | ||
| 475 | + "node_modules/@esbuild/linux-arm64": { | ||
| 476 | + "version": "0.21.5", | ||
| 477 | + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", | ||
| 478 | + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", | ||
| 479 | + "cpu": [ | ||
| 480 | + "arm64" | ||
| 481 | + ], | ||
| 482 | + "dev": true, | ||
| 483 | + "license": "MIT", | ||
| 484 | + "optional": true, | ||
| 485 | + "os": [ | ||
| 486 | + "linux" | ||
| 487 | + ], | ||
| 488 | + "engines": { | ||
| 489 | + "node": ">=12" | ||
| 490 | + } | ||
| 491 | + }, | ||
| 492 | + "node_modules/@esbuild/linux-ia32": { | ||
| 493 | + "version": "0.21.5", | ||
| 494 | + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", | ||
| 495 | + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", | ||
| 496 | + "cpu": [ | ||
| 497 | + "ia32" | ||
| 498 | + ], | ||
| 499 | + "dev": true, | ||
| 500 | + "license": "MIT", | ||
| 501 | + "optional": true, | ||
| 502 | + "os": [ | ||
| 503 | + "linux" | ||
| 504 | + ], | ||
| 505 | + "engines": { | ||
| 506 | + "node": ">=12" | ||
| 507 | + } | ||
| 508 | + }, | ||
| 509 | + "node_modules/@esbuild/linux-loong64": { | ||
| 510 | + "version": "0.21.5", | ||
| 511 | + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", | ||
| 512 | + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", | ||
| 513 | + "cpu": [ | ||
| 514 | + "loong64" | ||
| 515 | + ], | ||
| 516 | + "dev": true, | ||
| 517 | + "license": "MIT", | ||
| 518 | + "optional": true, | ||
| 519 | + "os": [ | ||
| 520 | + "linux" | ||
| 521 | + ], | ||
| 522 | + "engines": { | ||
| 523 | + "node": ">=12" | ||
| 524 | + } | ||
| 525 | + }, | ||
| 526 | + "node_modules/@esbuild/linux-mips64el": { | ||
| 527 | + "version": "0.21.5", | ||
| 528 | + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", | ||
| 529 | + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", | ||
| 530 | + "cpu": [ | ||
| 531 | + "mips64el" | ||
| 532 | + ], | ||
| 533 | + "dev": true, | ||
| 534 | + "license": "MIT", | ||
| 535 | + "optional": true, | ||
| 536 | + "os": [ | ||
| 537 | + "linux" | ||
| 538 | + ], | ||
| 539 | + "engines": { | ||
| 540 | + "node": ">=12" | ||
| 541 | + } | ||
| 542 | + }, | ||
| 543 | + "node_modules/@esbuild/linux-ppc64": { | ||
| 544 | + "version": "0.21.5", | ||
| 545 | + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", | ||
| 546 | + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", | ||
| 547 | + "cpu": [ | ||
| 548 | + "ppc64" | ||
| 549 | + ], | ||
| 550 | + "dev": true, | ||
| 551 | + "license": "MIT", | ||
| 552 | + "optional": true, | ||
| 553 | + "os": [ | ||
| 554 | + "linux" | ||
| 555 | + ], | ||
| 556 | + "engines": { | ||
| 557 | + "node": ">=12" | ||
| 558 | + } | ||
| 559 | + }, | ||
| 560 | + "node_modules/@esbuild/linux-riscv64": { | ||
| 561 | + "version": "0.21.5", | ||
| 562 | + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", | ||
| 563 | + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", | ||
| 564 | + "cpu": [ | ||
| 565 | + "riscv64" | ||
| 566 | + ], | ||
| 567 | + "dev": true, | ||
| 568 | + "license": "MIT", | ||
| 569 | + "optional": true, | ||
| 570 | + "os": [ | ||
| 571 | + "linux" | ||
| 572 | + ], | ||
| 573 | + "engines": { | ||
| 574 | + "node": ">=12" | ||
| 575 | + } | ||
| 576 | + }, | ||
| 577 | + "node_modules/@esbuild/linux-s390x": { | ||
| 578 | + "version": "0.21.5", | ||
| 579 | + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", | ||
| 580 | + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", | ||
| 581 | + "cpu": [ | ||
| 582 | + "s390x" | ||
| 583 | + ], | ||
| 584 | + "dev": true, | ||
| 585 | + "license": "MIT", | ||
| 586 | + "optional": true, | ||
| 587 | + "os": [ | ||
| 588 | + "linux" | ||
| 589 | + ], | ||
| 590 | + "engines": { | ||
| 591 | + "node": ">=12" | ||
| 592 | + } | ||
| 593 | + }, | ||
| 594 | + "node_modules/@esbuild/linux-x64": { | ||
| 595 | + "version": "0.21.5", | ||
| 596 | + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", | ||
| 597 | + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", | ||
| 598 | + "cpu": [ | ||
| 599 | + "x64" | ||
| 600 | + ], | ||
| 601 | + "dev": true, | ||
| 602 | + "license": "MIT", | ||
| 603 | + "optional": true, | ||
| 604 | + "os": [ | ||
| 605 | + "linux" | ||
| 606 | + ], | ||
| 607 | + "engines": { | ||
| 608 | + "node": ">=12" | ||
| 609 | + } | ||
| 610 | + }, | ||
| 611 | + "node_modules/@esbuild/netbsd-x64": { | ||
| 612 | + "version": "0.21.5", | ||
| 613 | + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", | ||
| 614 | + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", | ||
| 615 | + "cpu": [ | ||
| 616 | + "x64" | ||
| 617 | + ], | ||
| 618 | + "dev": true, | ||
| 619 | + "license": "MIT", | ||
| 620 | + "optional": true, | ||
| 621 | + "os": [ | ||
| 622 | + "netbsd" | ||
| 623 | + ], | ||
| 624 | + "engines": { | ||
| 625 | + "node": ">=12" | ||
| 626 | + } | ||
| 627 | + }, | ||
| 628 | + "node_modules/@esbuild/openbsd-x64": { | ||
| 629 | + "version": "0.21.5", | ||
| 630 | + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", | ||
| 631 | + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", | ||
| 632 | + "cpu": [ | ||
| 633 | + "x64" | ||
| 634 | + ], | ||
| 635 | + "dev": true, | ||
| 636 | + "license": "MIT", | ||
| 637 | + "optional": true, | ||
| 638 | + "os": [ | ||
| 639 | + "openbsd" | ||
| 640 | + ], | ||
| 641 | + "engines": { | ||
| 642 | + "node": ">=12" | ||
| 643 | + } | ||
| 644 | + }, | ||
| 645 | + "node_modules/@esbuild/sunos-x64": { | ||
| 646 | + "version": "0.21.5", | ||
| 647 | + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", | ||
| 648 | + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", | ||
| 649 | + "cpu": [ | ||
| 650 | + "x64" | ||
| 651 | + ], | ||
| 652 | + "dev": true, | ||
| 653 | + "license": "MIT", | ||
| 654 | + "optional": true, | ||
| 655 | + "os": [ | ||
| 656 | + "sunos" | ||
| 657 | + ], | ||
| 658 | + "engines": { | ||
| 659 | + "node": ">=12" | ||
| 660 | + } | ||
| 661 | + }, | ||
| 662 | + "node_modules/@esbuild/win32-arm64": { | ||
| 663 | + "version": "0.21.5", | ||
| 664 | + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", | ||
| 665 | + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", | ||
| 666 | + "cpu": [ | ||
| 667 | + "arm64" | ||
| 668 | + ], | ||
| 669 | + "dev": true, | ||
| 670 | + "license": "MIT", | ||
| 671 | + "optional": true, | ||
| 672 | + "os": [ | ||
| 673 | + "win32" | ||
| 674 | + ], | ||
| 675 | + "engines": { | ||
| 676 | + "node": ">=12" | ||
| 677 | + } | ||
| 678 | + }, | ||
| 679 | + "node_modules/@esbuild/win32-ia32": { | ||
| 680 | + "version": "0.21.5", | ||
| 681 | + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", | ||
| 682 | + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", | ||
| 683 | + "cpu": [ | ||
| 684 | + "ia32" | ||
| 685 | + ], | ||
| 686 | + "dev": true, | ||
| 687 | + "license": "MIT", | ||
| 688 | + "optional": true, | ||
| 689 | + "os": [ | ||
| 690 | + "win32" | ||
| 691 | + ], | ||
| 692 | + "engines": { | ||
| 693 | + "node": ">=12" | ||
| 694 | + } | ||
| 695 | + }, | ||
| 696 | + "node_modules/@esbuild/win32-x64": { | ||
| 697 | + "version": "0.21.5", | ||
| 698 | + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", | ||
| 699 | + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", | ||
| 700 | + "cpu": [ | ||
| 701 | + "x64" | ||
| 702 | + ], | ||
| 703 | + "dev": true, | ||
| 704 | + "license": "MIT", | ||
| 705 | + "optional": true, | ||
| 706 | + "os": [ | ||
| 707 | + "win32" | ||
| 708 | + ], | ||
| 709 | + "engines": { | ||
| 710 | + "node": ">=12" | ||
| 711 | + } | ||
| 712 | + }, | ||
| 713 | + "node_modules/@jridgewell/gen-mapping": { | ||
| 714 | + "version": "0.3.13", | ||
| 715 | + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", | ||
| 716 | + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", | ||
| 717 | + "dev": true, | ||
| 718 | + "license": "MIT", | ||
| 719 | + "dependencies": { | ||
| 720 | + "@jridgewell/sourcemap-codec": "^1.5.0", | ||
| 721 | + "@jridgewell/trace-mapping": "^0.3.24" | ||
| 722 | + } | ||
| 723 | + }, | ||
| 724 | + "node_modules/@jridgewell/remapping": { | ||
| 725 | + "version": "2.3.5", | ||
| 726 | + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", | ||
| 727 | + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", | ||
| 728 | + "dev": true, | ||
| 729 | + "license": "MIT", | ||
| 730 | + "dependencies": { | ||
| 731 | + "@jridgewell/gen-mapping": "^0.3.5", | ||
| 732 | + "@jridgewell/trace-mapping": "^0.3.24" | ||
| 733 | + } | ||
| 734 | + }, | ||
| 735 | + "node_modules/@jridgewell/resolve-uri": { | ||
| 736 | + "version": "3.1.2", | ||
| 737 | + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", | ||
| 738 | + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", | ||
| 739 | + "dev": true, | ||
| 740 | + "license": "MIT", | ||
| 741 | + "engines": { | ||
| 742 | + "node": ">=6.0.0" | ||
| 743 | + } | ||
| 744 | + }, | ||
| 745 | + "node_modules/@jridgewell/sourcemap-codec": { | ||
| 746 | + "version": "1.5.5", | ||
| 747 | + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", | ||
| 748 | + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", | ||
| 749 | + "dev": true, | ||
| 750 | + "license": "MIT" | ||
| 751 | + }, | ||
| 752 | + "node_modules/@jridgewell/trace-mapping": { | ||
| 753 | + "version": "0.3.31", | ||
| 754 | + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", | ||
| 755 | + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", | ||
| 756 | + "dev": true, | ||
| 757 | + "license": "MIT", | ||
| 758 | + "dependencies": { | ||
| 759 | + "@jridgewell/resolve-uri": "^3.1.0", | ||
| 760 | + "@jridgewell/sourcemap-codec": "^1.4.14" | ||
| 761 | + } | ||
| 762 | + }, | ||
| 763 | + "node_modules/@nodelib/fs.scandir": { | ||
| 764 | + "version": "2.1.5", | ||
| 765 | + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | ||
| 766 | + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", | ||
| 767 | + "dev": true, | ||
| 768 | + "license": "MIT", | ||
| 769 | + "dependencies": { | ||
| 770 | + "@nodelib/fs.stat": "2.0.5", | ||
| 771 | + "run-parallel": "^1.1.9" | ||
| 772 | + }, | ||
| 773 | + "engines": { | ||
| 774 | + "node": ">= 8" | ||
| 775 | + } | ||
| 776 | + }, | ||
| 777 | + "node_modules/@nodelib/fs.stat": { | ||
| 778 | + "version": "2.0.5", | ||
| 779 | + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", | ||
| 780 | + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", | ||
| 781 | + "dev": true, | ||
| 782 | + "license": "MIT", | ||
| 783 | + "engines": { | ||
| 784 | + "node": ">= 8" | ||
| 785 | + } | ||
| 786 | + }, | ||
| 787 | + "node_modules/@nodelib/fs.walk": { | ||
| 788 | + "version": "1.2.8", | ||
| 789 | + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", | ||
| 790 | + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", | ||
| 791 | + "dev": true, | ||
| 792 | + "license": "MIT", | ||
| 793 | + "dependencies": { | ||
| 794 | + "@nodelib/fs.scandir": "2.1.5", | ||
| 795 | + "fastq": "^1.6.0" | ||
| 796 | + }, | ||
| 797 | + "engines": { | ||
| 798 | + "node": ">= 8" | ||
| 799 | + } | ||
| 800 | + }, | ||
| 801 | + "node_modules/@remix-run/router": { | ||
| 802 | + "version": "1.23.2", | ||
| 803 | + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", | ||
| 804 | + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", | ||
| 805 | + "license": "MIT", | ||
| 806 | + "engines": { | ||
| 807 | + "node": ">=14.0.0" | ||
| 808 | + } | ||
| 809 | + }, | ||
| 810 | + "node_modules/@rolldown/pluginutils": { | ||
| 811 | + "version": "1.0.0-beta.27", | ||
| 812 | + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", | ||
| 813 | + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", | ||
| 814 | + "dev": true, | ||
| 815 | + "license": "MIT" | ||
| 816 | + }, | ||
| 817 | + "node_modules/@rollup/rollup-android-arm-eabi": { | ||
| 818 | + "version": "4.60.1", | ||
| 819 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", | ||
| 820 | + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", | ||
| 821 | + "cpu": [ | ||
| 822 | + "arm" | ||
| 823 | + ], | ||
| 824 | + "dev": true, | ||
| 825 | + "license": "MIT", | ||
| 826 | + "optional": true, | ||
| 827 | + "os": [ | ||
| 828 | + "android" | ||
| 829 | + ] | ||
| 830 | + }, | ||
| 831 | + "node_modules/@rollup/rollup-android-arm64": { | ||
| 832 | + "version": "4.60.1", | ||
| 833 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", | ||
| 834 | + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", | ||
| 835 | + "cpu": [ | ||
| 836 | + "arm64" | ||
| 837 | + ], | ||
| 838 | + "dev": true, | ||
| 839 | + "license": "MIT", | ||
| 840 | + "optional": true, | ||
| 841 | + "os": [ | ||
| 842 | + "android" | ||
| 843 | + ] | ||
| 844 | + }, | ||
| 845 | + "node_modules/@rollup/rollup-darwin-arm64": { | ||
| 846 | + "version": "4.60.1", | ||
| 847 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", | ||
| 848 | + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", | ||
| 849 | + "cpu": [ | ||
| 850 | + "arm64" | ||
| 851 | + ], | ||
| 852 | + "dev": true, | ||
| 853 | + "license": "MIT", | ||
| 854 | + "optional": true, | ||
| 855 | + "os": [ | ||
| 856 | + "darwin" | ||
| 857 | + ] | ||
| 858 | + }, | ||
| 859 | + "node_modules/@rollup/rollup-darwin-x64": { | ||
| 860 | + "version": "4.60.1", | ||
| 861 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", | ||
| 862 | + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", | ||
| 863 | + "cpu": [ | ||
| 864 | + "x64" | ||
| 865 | + ], | ||
| 866 | + "dev": true, | ||
| 867 | + "license": "MIT", | ||
| 868 | + "optional": true, | ||
| 869 | + "os": [ | ||
| 870 | + "darwin" | ||
| 871 | + ] | ||
| 872 | + }, | ||
| 873 | + "node_modules/@rollup/rollup-freebsd-arm64": { | ||
| 874 | + "version": "4.60.1", | ||
| 875 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", | ||
| 876 | + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", | ||
| 877 | + "cpu": [ | ||
| 878 | + "arm64" | ||
| 879 | + ], | ||
| 880 | + "dev": true, | ||
| 881 | + "license": "MIT", | ||
| 882 | + "optional": true, | ||
| 883 | + "os": [ | ||
| 884 | + "freebsd" | ||
| 885 | + ] | ||
| 886 | + }, | ||
| 887 | + "node_modules/@rollup/rollup-freebsd-x64": { | ||
| 888 | + "version": "4.60.1", | ||
| 889 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", | ||
| 890 | + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", | ||
| 891 | + "cpu": [ | ||
| 892 | + "x64" | ||
| 893 | + ], | ||
| 894 | + "dev": true, | ||
| 895 | + "license": "MIT", | ||
| 896 | + "optional": true, | ||
| 897 | + "os": [ | ||
| 898 | + "freebsd" | ||
| 899 | + ] | ||
| 900 | + }, | ||
| 901 | + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { | ||
| 902 | + "version": "4.60.1", | ||
| 903 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", | ||
| 904 | + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", | ||
| 905 | + "cpu": [ | ||
| 906 | + "arm" | ||
| 907 | + ], | ||
| 908 | + "dev": true, | ||
| 909 | + "libc": [ | ||
| 910 | + "glibc" | ||
| 911 | + ], | ||
| 912 | + "license": "MIT", | ||
| 913 | + "optional": true, | ||
| 914 | + "os": [ | ||
| 915 | + "linux" | ||
| 916 | + ] | ||
| 917 | + }, | ||
| 918 | + "node_modules/@rollup/rollup-linux-arm-musleabihf": { | ||
| 919 | + "version": "4.60.1", | ||
| 920 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", | ||
| 921 | + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", | ||
| 922 | + "cpu": [ | ||
| 923 | + "arm" | ||
| 924 | + ], | ||
| 925 | + "dev": true, | ||
| 926 | + "libc": [ | ||
| 927 | + "musl" | ||
| 928 | + ], | ||
| 929 | + "license": "MIT", | ||
| 930 | + "optional": true, | ||
| 931 | + "os": [ | ||
| 932 | + "linux" | ||
| 933 | + ] | ||
| 934 | + }, | ||
| 935 | + "node_modules/@rollup/rollup-linux-arm64-gnu": { | ||
| 936 | + "version": "4.60.1", | ||
| 937 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", | ||
| 938 | + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", | ||
| 939 | + "cpu": [ | ||
| 940 | + "arm64" | ||
| 941 | + ], | ||
| 942 | + "dev": true, | ||
| 943 | + "libc": [ | ||
| 944 | + "glibc" | ||
| 945 | + ], | ||
| 946 | + "license": "MIT", | ||
| 947 | + "optional": true, | ||
| 948 | + "os": [ | ||
| 949 | + "linux" | ||
| 950 | + ] | ||
| 951 | + }, | ||
| 952 | + "node_modules/@rollup/rollup-linux-arm64-musl": { | ||
| 953 | + "version": "4.60.1", | ||
| 954 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", | ||
| 955 | + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", | ||
| 956 | + "cpu": [ | ||
| 957 | + "arm64" | ||
| 958 | + ], | ||
| 959 | + "dev": true, | ||
| 960 | + "libc": [ | ||
| 961 | + "musl" | ||
| 962 | + ], | ||
| 963 | + "license": "MIT", | ||
| 964 | + "optional": true, | ||
| 965 | + "os": [ | ||
| 966 | + "linux" | ||
| 967 | + ] | ||
| 968 | + }, | ||
| 969 | + "node_modules/@rollup/rollup-linux-loong64-gnu": { | ||
| 970 | + "version": "4.60.1", | ||
| 971 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", | ||
| 972 | + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", | ||
| 973 | + "cpu": [ | ||
| 974 | + "loong64" | ||
| 975 | + ], | ||
| 976 | + "dev": true, | ||
| 977 | + "libc": [ | ||
| 978 | + "glibc" | ||
| 979 | + ], | ||
| 980 | + "license": "MIT", | ||
| 981 | + "optional": true, | ||
| 982 | + "os": [ | ||
| 983 | + "linux" | ||
| 984 | + ] | ||
| 985 | + }, | ||
| 986 | + "node_modules/@rollup/rollup-linux-loong64-musl": { | ||
| 987 | + "version": "4.60.1", | ||
| 988 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", | ||
| 989 | + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", | ||
| 990 | + "cpu": [ | ||
| 991 | + "loong64" | ||
| 992 | + ], | ||
| 993 | + "dev": true, | ||
| 994 | + "libc": [ | ||
| 995 | + "musl" | ||
| 996 | + ], | ||
| 997 | + "license": "MIT", | ||
| 998 | + "optional": true, | ||
| 999 | + "os": [ | ||
| 1000 | + "linux" | ||
| 1001 | + ] | ||
| 1002 | + }, | ||
| 1003 | + "node_modules/@rollup/rollup-linux-ppc64-gnu": { | ||
| 1004 | + "version": "4.60.1", | ||
| 1005 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", | ||
| 1006 | + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", | ||
| 1007 | + "cpu": [ | ||
| 1008 | + "ppc64" | ||
| 1009 | + ], | ||
| 1010 | + "dev": true, | ||
| 1011 | + "libc": [ | ||
| 1012 | + "glibc" | ||
| 1013 | + ], | ||
| 1014 | + "license": "MIT", | ||
| 1015 | + "optional": true, | ||
| 1016 | + "os": [ | ||
| 1017 | + "linux" | ||
| 1018 | + ] | ||
| 1019 | + }, | ||
| 1020 | + "node_modules/@rollup/rollup-linux-ppc64-musl": { | ||
| 1021 | + "version": "4.60.1", | ||
| 1022 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", | ||
| 1023 | + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", | ||
| 1024 | + "cpu": [ | ||
| 1025 | + "ppc64" | ||
| 1026 | + ], | ||
| 1027 | + "dev": true, | ||
| 1028 | + "libc": [ | ||
| 1029 | + "musl" | ||
| 1030 | + ], | ||
| 1031 | + "license": "MIT", | ||
| 1032 | + "optional": true, | ||
| 1033 | + "os": [ | ||
| 1034 | + "linux" | ||
| 1035 | + ] | ||
| 1036 | + }, | ||
| 1037 | + "node_modules/@rollup/rollup-linux-riscv64-gnu": { | ||
| 1038 | + "version": "4.60.1", | ||
| 1039 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", | ||
| 1040 | + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", | ||
| 1041 | + "cpu": [ | ||
| 1042 | + "riscv64" | ||
| 1043 | + ], | ||
| 1044 | + "dev": true, | ||
| 1045 | + "libc": [ | ||
| 1046 | + "glibc" | ||
| 1047 | + ], | ||
| 1048 | + "license": "MIT", | ||
| 1049 | + "optional": true, | ||
| 1050 | + "os": [ | ||
| 1051 | + "linux" | ||
| 1052 | + ] | ||
| 1053 | + }, | ||
| 1054 | + "node_modules/@rollup/rollup-linux-riscv64-musl": { | ||
| 1055 | + "version": "4.60.1", | ||
| 1056 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", | ||
| 1057 | + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", | ||
| 1058 | + "cpu": [ | ||
| 1059 | + "riscv64" | ||
| 1060 | + ], | ||
| 1061 | + "dev": true, | ||
| 1062 | + "libc": [ | ||
| 1063 | + "musl" | ||
| 1064 | + ], | ||
| 1065 | + "license": "MIT", | ||
| 1066 | + "optional": true, | ||
| 1067 | + "os": [ | ||
| 1068 | + "linux" | ||
| 1069 | + ] | ||
| 1070 | + }, | ||
| 1071 | + "node_modules/@rollup/rollup-linux-s390x-gnu": { | ||
| 1072 | + "version": "4.60.1", | ||
| 1073 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", | ||
| 1074 | + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", | ||
| 1075 | + "cpu": [ | ||
| 1076 | + "s390x" | ||
| 1077 | + ], | ||
| 1078 | + "dev": true, | ||
| 1079 | + "libc": [ | ||
| 1080 | + "glibc" | ||
| 1081 | + ], | ||
| 1082 | + "license": "MIT", | ||
| 1083 | + "optional": true, | ||
| 1084 | + "os": [ | ||
| 1085 | + "linux" | ||
| 1086 | + ] | ||
| 1087 | + }, | ||
| 1088 | + "node_modules/@rollup/rollup-linux-x64-gnu": { | ||
| 1089 | + "version": "4.60.1", | ||
| 1090 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", | ||
| 1091 | + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", | ||
| 1092 | + "cpu": [ | ||
| 1093 | + "x64" | ||
| 1094 | + ], | ||
| 1095 | + "dev": true, | ||
| 1096 | + "libc": [ | ||
| 1097 | + "glibc" | ||
| 1098 | + ], | ||
| 1099 | + "license": "MIT", | ||
| 1100 | + "optional": true, | ||
| 1101 | + "os": [ | ||
| 1102 | + "linux" | ||
| 1103 | + ] | ||
| 1104 | + }, | ||
| 1105 | + "node_modules/@rollup/rollup-linux-x64-musl": { | ||
| 1106 | + "version": "4.60.1", | ||
| 1107 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", | ||
| 1108 | + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", | ||
| 1109 | + "cpu": [ | ||
| 1110 | + "x64" | ||
| 1111 | + ], | ||
| 1112 | + "dev": true, | ||
| 1113 | + "libc": [ | ||
| 1114 | + "musl" | ||
| 1115 | + ], | ||
| 1116 | + "license": "MIT", | ||
| 1117 | + "optional": true, | ||
| 1118 | + "os": [ | ||
| 1119 | + "linux" | ||
| 1120 | + ] | ||
| 1121 | + }, | ||
| 1122 | + "node_modules/@rollup/rollup-openbsd-x64": { | ||
| 1123 | + "version": "4.60.1", | ||
| 1124 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", | ||
| 1125 | + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", | ||
| 1126 | + "cpu": [ | ||
| 1127 | + "x64" | ||
| 1128 | + ], | ||
| 1129 | + "dev": true, | ||
| 1130 | + "license": "MIT", | ||
| 1131 | + "optional": true, | ||
| 1132 | + "os": [ | ||
| 1133 | + "openbsd" | ||
| 1134 | + ] | ||
| 1135 | + }, | ||
| 1136 | + "node_modules/@rollup/rollup-openharmony-arm64": { | ||
| 1137 | + "version": "4.60.1", | ||
| 1138 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", | ||
| 1139 | + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", | ||
| 1140 | + "cpu": [ | ||
| 1141 | + "arm64" | ||
| 1142 | + ], | ||
| 1143 | + "dev": true, | ||
| 1144 | + "license": "MIT", | ||
| 1145 | + "optional": true, | ||
| 1146 | + "os": [ | ||
| 1147 | + "openharmony" | ||
| 1148 | + ] | ||
| 1149 | + }, | ||
| 1150 | + "node_modules/@rollup/rollup-win32-arm64-msvc": { | ||
| 1151 | + "version": "4.60.1", | ||
| 1152 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", | ||
| 1153 | + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", | ||
| 1154 | + "cpu": [ | ||
| 1155 | + "arm64" | ||
| 1156 | + ], | ||
| 1157 | + "dev": true, | ||
| 1158 | + "license": "MIT", | ||
| 1159 | + "optional": true, | ||
| 1160 | + "os": [ | ||
| 1161 | + "win32" | ||
| 1162 | + ] | ||
| 1163 | + }, | ||
| 1164 | + "node_modules/@rollup/rollup-win32-ia32-msvc": { | ||
| 1165 | + "version": "4.60.1", | ||
| 1166 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", | ||
| 1167 | + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", | ||
| 1168 | + "cpu": [ | ||
| 1169 | + "ia32" | ||
| 1170 | + ], | ||
| 1171 | + "dev": true, | ||
| 1172 | + "license": "MIT", | ||
| 1173 | + "optional": true, | ||
| 1174 | + "os": [ | ||
| 1175 | + "win32" | ||
| 1176 | + ] | ||
| 1177 | + }, | ||
| 1178 | + "node_modules/@rollup/rollup-win32-x64-gnu": { | ||
| 1179 | + "version": "4.60.1", | ||
| 1180 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", | ||
| 1181 | + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", | ||
| 1182 | + "cpu": [ | ||
| 1183 | + "x64" | ||
| 1184 | + ], | ||
| 1185 | + "dev": true, | ||
| 1186 | + "license": "MIT", | ||
| 1187 | + "optional": true, | ||
| 1188 | + "os": [ | ||
| 1189 | + "win32" | ||
| 1190 | + ] | ||
| 1191 | + }, | ||
| 1192 | + "node_modules/@rollup/rollup-win32-x64-msvc": { | ||
| 1193 | + "version": "4.60.1", | ||
| 1194 | + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", | ||
| 1195 | + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", | ||
| 1196 | + "cpu": [ | ||
| 1197 | + "x64" | ||
| 1198 | + ], | ||
| 1199 | + "dev": true, | ||
| 1200 | + "license": "MIT", | ||
| 1201 | + "optional": true, | ||
| 1202 | + "os": [ | ||
| 1203 | + "win32" | ||
| 1204 | + ] | ||
| 1205 | + }, | ||
| 1206 | + "node_modules/@types/babel__core": { | ||
| 1207 | + "version": "7.20.5", | ||
| 1208 | + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", | ||
| 1209 | + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", | ||
| 1210 | + "dev": true, | ||
| 1211 | + "license": "MIT", | ||
| 1212 | + "dependencies": { | ||
| 1213 | + "@babel/parser": "^7.20.7", | ||
| 1214 | + "@babel/types": "^7.20.7", | ||
| 1215 | + "@types/babel__generator": "*", | ||
| 1216 | + "@types/babel__template": "*", | ||
| 1217 | + "@types/babel__traverse": "*" | ||
| 1218 | + } | ||
| 1219 | + }, | ||
| 1220 | + "node_modules/@types/babel__generator": { | ||
| 1221 | + "version": "7.27.0", | ||
| 1222 | + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", | ||
| 1223 | + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", | ||
| 1224 | + "dev": true, | ||
| 1225 | + "license": "MIT", | ||
| 1226 | + "dependencies": { | ||
| 1227 | + "@babel/types": "^7.0.0" | ||
| 1228 | + } | ||
| 1229 | + }, | ||
| 1230 | + "node_modules/@types/babel__template": { | ||
| 1231 | + "version": "7.4.4", | ||
| 1232 | + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", | ||
| 1233 | + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", | ||
| 1234 | + "dev": true, | ||
| 1235 | + "license": "MIT", | ||
| 1236 | + "dependencies": { | ||
| 1237 | + "@babel/parser": "^7.1.0", | ||
| 1238 | + "@babel/types": "^7.0.0" | ||
| 1239 | + } | ||
| 1240 | + }, | ||
| 1241 | + "node_modules/@types/babel__traverse": { | ||
| 1242 | + "version": "7.28.0", | ||
| 1243 | + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", | ||
| 1244 | + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", | ||
| 1245 | + "dev": true, | ||
| 1246 | + "license": "MIT", | ||
| 1247 | + "dependencies": { | ||
| 1248 | + "@babel/types": "^7.28.2" | ||
| 1249 | + } | ||
| 1250 | + }, | ||
| 1251 | + "node_modules/@types/estree": { | ||
| 1252 | + "version": "1.0.8", | ||
| 1253 | + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", | ||
| 1254 | + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", | ||
| 1255 | + "dev": true, | ||
| 1256 | + "license": "MIT" | ||
| 1257 | + }, | ||
| 1258 | + "node_modules/@types/node": { | ||
| 1259 | + "version": "25.5.2", | ||
| 1260 | + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", | ||
| 1261 | + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", | ||
| 1262 | + "dev": true, | ||
| 1263 | + "license": "MIT", | ||
| 1264 | + "dependencies": { | ||
| 1265 | + "undici-types": "~7.18.0" | ||
| 1266 | + } | ||
| 1267 | + }, | ||
| 1268 | + "node_modules/@types/prop-types": { | ||
| 1269 | + "version": "15.7.15", | ||
| 1270 | + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", | ||
| 1271 | + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", | ||
| 1272 | + "dev": true, | ||
| 1273 | + "license": "MIT" | ||
| 1274 | + }, | ||
| 1275 | + "node_modules/@types/react": { | ||
| 1276 | + "version": "18.3.28", | ||
| 1277 | + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", | ||
| 1278 | + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", | ||
| 1279 | + "dev": true, | ||
| 1280 | + "license": "MIT", | ||
| 1281 | + "dependencies": { | ||
| 1282 | + "@types/prop-types": "*", | ||
| 1283 | + "csstype": "^3.2.2" | ||
| 1284 | + } | ||
| 1285 | + }, | ||
| 1286 | + "node_modules/@types/react-dom": { | ||
| 1287 | + "version": "18.3.7", | ||
| 1288 | + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", | ||
| 1289 | + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", | ||
| 1290 | + "dev": true, | ||
| 1291 | + "license": "MIT", | ||
| 1292 | + "peerDependencies": { | ||
| 1293 | + "@types/react": "^18.0.0" | ||
| 1294 | + } | ||
| 1295 | + }, | ||
| 1296 | + "node_modules/@vitejs/plugin-react": { | ||
| 1297 | + "version": "4.7.0", | ||
| 1298 | + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", | ||
| 1299 | + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", | ||
| 1300 | + "dev": true, | ||
| 1301 | + "license": "MIT", | ||
| 1302 | + "dependencies": { | ||
| 1303 | + "@babel/core": "^7.28.0", | ||
| 1304 | + "@babel/plugin-transform-react-jsx-self": "^7.27.1", | ||
| 1305 | + "@babel/plugin-transform-react-jsx-source": "^7.27.1", | ||
| 1306 | + "@rolldown/pluginutils": "1.0.0-beta.27", | ||
| 1307 | + "@types/babel__core": "^7.20.5", | ||
| 1308 | + "react-refresh": "^0.17.0" | ||
| 1309 | + }, | ||
| 1310 | + "engines": { | ||
| 1311 | + "node": "^14.18.0 || >=16.0.0" | ||
| 1312 | + }, | ||
| 1313 | + "peerDependencies": { | ||
| 1314 | + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" | ||
| 1315 | + } | ||
| 1316 | + }, | ||
| 1317 | + "node_modules/any-promise": { | ||
| 1318 | + "version": "1.3.0", | ||
| 1319 | + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", | ||
| 1320 | + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", | ||
| 1321 | + "dev": true, | ||
| 1322 | + "license": "MIT" | ||
| 1323 | + }, | ||
| 1324 | + "node_modules/anymatch": { | ||
| 1325 | + "version": "3.1.3", | ||
| 1326 | + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", | ||
| 1327 | + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", | ||
| 1328 | + "dev": true, | ||
| 1329 | + "license": "ISC", | ||
| 1330 | + "dependencies": { | ||
| 1331 | + "normalize-path": "^3.0.0", | ||
| 1332 | + "picomatch": "^2.0.4" | ||
| 1333 | + }, | ||
| 1334 | + "engines": { | ||
| 1335 | + "node": ">= 8" | ||
| 1336 | + } | ||
| 1337 | + }, | ||
| 1338 | + "node_modules/arg": { | ||
| 1339 | + "version": "5.0.2", | ||
| 1340 | + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", | ||
| 1341 | + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", | ||
| 1342 | + "dev": true, | ||
| 1343 | + "license": "MIT" | ||
| 1344 | + }, | ||
| 1345 | + "node_modules/autoprefixer": { | ||
| 1346 | + "version": "10.4.27", | ||
| 1347 | + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", | ||
| 1348 | + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", | ||
| 1349 | + "dev": true, | ||
| 1350 | + "funding": [ | ||
| 1351 | + { | ||
| 1352 | + "type": "opencollective", | ||
| 1353 | + "url": "https://opencollective.com/postcss/" | ||
| 1354 | + }, | ||
| 1355 | + { | ||
| 1356 | + "type": "tidelift", | ||
| 1357 | + "url": "https://tidelift.com/funding/github/npm/autoprefixer" | ||
| 1358 | + }, | ||
| 1359 | + { | ||
| 1360 | + "type": "github", | ||
| 1361 | + "url": "https://github.com/sponsors/ai" | ||
| 1362 | + } | ||
| 1363 | + ], | ||
| 1364 | + "license": "MIT", | ||
| 1365 | + "dependencies": { | ||
| 1366 | + "browserslist": "^4.28.1", | ||
| 1367 | + "caniuse-lite": "^1.0.30001774", | ||
| 1368 | + "fraction.js": "^5.3.4", | ||
| 1369 | + "picocolors": "^1.1.1", | ||
| 1370 | + "postcss-value-parser": "^4.2.0" | ||
| 1371 | + }, | ||
| 1372 | + "bin": { | ||
| 1373 | + "autoprefixer": "bin/autoprefixer" | ||
| 1374 | + }, | ||
| 1375 | + "engines": { | ||
| 1376 | + "node": "^10 || ^12 || >=14" | ||
| 1377 | + }, | ||
| 1378 | + "peerDependencies": { | ||
| 1379 | + "postcss": "^8.1.0" | ||
| 1380 | + } | ||
| 1381 | + }, | ||
| 1382 | + "node_modules/baseline-browser-mapping": { | ||
| 1383 | + "version": "2.10.16", | ||
| 1384 | + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", | ||
| 1385 | + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", | ||
| 1386 | + "dev": true, | ||
| 1387 | + "license": "Apache-2.0", | ||
| 1388 | + "bin": { | ||
| 1389 | + "baseline-browser-mapping": "dist/cli.cjs" | ||
| 1390 | + }, | ||
| 1391 | + "engines": { | ||
| 1392 | + "node": ">=6.0.0" | ||
| 1393 | + } | ||
| 1394 | + }, | ||
| 1395 | + "node_modules/binary-extensions": { | ||
| 1396 | + "version": "2.3.0", | ||
| 1397 | + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", | ||
| 1398 | + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", | ||
| 1399 | + "dev": true, | ||
| 1400 | + "license": "MIT", | ||
| 1401 | + "engines": { | ||
| 1402 | + "node": ">=8" | ||
| 1403 | + }, | ||
| 1404 | + "funding": { | ||
| 1405 | + "url": "https://github.com/sponsors/sindresorhus" | ||
| 1406 | + } | ||
| 1407 | + }, | ||
| 1408 | + "node_modules/braces": { | ||
| 1409 | + "version": "3.0.3", | ||
| 1410 | + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", | ||
| 1411 | + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", | ||
| 1412 | + "dev": true, | ||
| 1413 | + "license": "MIT", | ||
| 1414 | + "dependencies": { | ||
| 1415 | + "fill-range": "^7.1.1" | ||
| 1416 | + }, | ||
| 1417 | + "engines": { | ||
| 1418 | + "node": ">=8" | ||
| 1419 | + } | ||
| 1420 | + }, | ||
| 1421 | + "node_modules/browserslist": { | ||
| 1422 | + "version": "4.28.2", | ||
| 1423 | + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", | ||
| 1424 | + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", | ||
| 1425 | + "dev": true, | ||
| 1426 | + "funding": [ | ||
| 1427 | + { | ||
| 1428 | + "type": "opencollective", | ||
| 1429 | + "url": "https://opencollective.com/browserslist" | ||
| 1430 | + }, | ||
| 1431 | + { | ||
| 1432 | + "type": "tidelift", | ||
| 1433 | + "url": "https://tidelift.com/funding/github/npm/browserslist" | ||
| 1434 | + }, | ||
| 1435 | + { | ||
| 1436 | + "type": "github", | ||
| 1437 | + "url": "https://github.com/sponsors/ai" | ||
| 1438 | + } | ||
| 1439 | + ], | ||
| 1440 | + "license": "MIT", | ||
| 1441 | + "dependencies": { | ||
| 1442 | + "baseline-browser-mapping": "^2.10.12", | ||
| 1443 | + "caniuse-lite": "^1.0.30001782", | ||
| 1444 | + "electron-to-chromium": "^1.5.328", | ||
| 1445 | + "node-releases": "^2.0.36", | ||
| 1446 | + "update-browserslist-db": "^1.2.3" | ||
| 1447 | + }, | ||
| 1448 | + "bin": { | ||
| 1449 | + "browserslist": "cli.js" | ||
| 1450 | + }, | ||
| 1451 | + "engines": { | ||
| 1452 | + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" | ||
| 1453 | + } | ||
| 1454 | + }, | ||
| 1455 | + "node_modules/camelcase-css": { | ||
| 1456 | + "version": "2.0.1", | ||
| 1457 | + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", | ||
| 1458 | + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", | ||
| 1459 | + "dev": true, | ||
| 1460 | + "license": "MIT", | ||
| 1461 | + "engines": { | ||
| 1462 | + "node": ">= 6" | ||
| 1463 | + } | ||
| 1464 | + }, | ||
| 1465 | + "node_modules/caniuse-lite": { | ||
| 1466 | + "version": "1.0.30001787", | ||
| 1467 | + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", | ||
| 1468 | + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", | ||
| 1469 | + "dev": true, | ||
| 1470 | + "funding": [ | ||
| 1471 | + { | ||
| 1472 | + "type": "opencollective", | ||
| 1473 | + "url": "https://opencollective.com/browserslist" | ||
| 1474 | + }, | ||
| 1475 | + { | ||
| 1476 | + "type": "tidelift", | ||
| 1477 | + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" | ||
| 1478 | + }, | ||
| 1479 | + { | ||
| 1480 | + "type": "github", | ||
| 1481 | + "url": "https://github.com/sponsors/ai" | ||
| 1482 | + } | ||
| 1483 | + ], | ||
| 1484 | + "license": "CC-BY-4.0" | ||
| 1485 | + }, | ||
| 1486 | + "node_modules/chokidar": { | ||
| 1487 | + "version": "3.6.0", | ||
| 1488 | + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", | ||
| 1489 | + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", | ||
| 1490 | + "dev": true, | ||
| 1491 | + "license": "MIT", | ||
| 1492 | + "dependencies": { | ||
| 1493 | + "anymatch": "~3.1.2", | ||
| 1494 | + "braces": "~3.0.2", | ||
| 1495 | + "glob-parent": "~5.1.2", | ||
| 1496 | + "is-binary-path": "~2.1.0", | ||
| 1497 | + "is-glob": "~4.0.1", | ||
| 1498 | + "normalize-path": "~3.0.0", | ||
| 1499 | + "readdirp": "~3.6.0" | ||
| 1500 | + }, | ||
| 1501 | + "engines": { | ||
| 1502 | + "node": ">= 8.10.0" | ||
| 1503 | + }, | ||
| 1504 | + "funding": { | ||
| 1505 | + "url": "https://paulmillr.com/funding/" | ||
| 1506 | + }, | ||
| 1507 | + "optionalDependencies": { | ||
| 1508 | + "fsevents": "~2.3.2" | ||
| 1509 | + } | ||
| 1510 | + }, | ||
| 1511 | + "node_modules/chokidar/node_modules/glob-parent": { | ||
| 1512 | + "version": "5.1.2", | ||
| 1513 | + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", | ||
| 1514 | + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", | ||
| 1515 | + "dev": true, | ||
| 1516 | + "license": "ISC", | ||
| 1517 | + "dependencies": { | ||
| 1518 | + "is-glob": "^4.0.1" | ||
| 1519 | + }, | ||
| 1520 | + "engines": { | ||
| 1521 | + "node": ">= 6" | ||
| 1522 | + } | ||
| 1523 | + }, | ||
| 1524 | + "node_modules/commander": { | ||
| 1525 | + "version": "4.1.1", | ||
| 1526 | + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", | ||
| 1527 | + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", | ||
| 1528 | + "dev": true, | ||
| 1529 | + "license": "MIT", | ||
| 1530 | + "engines": { | ||
| 1531 | + "node": ">= 6" | ||
| 1532 | + } | ||
| 1533 | + }, | ||
| 1534 | + "node_modules/convert-source-map": { | ||
| 1535 | + "version": "2.0.0", | ||
| 1536 | + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", | ||
| 1537 | + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", | ||
| 1538 | + "dev": true, | ||
| 1539 | + "license": "MIT" | ||
| 1540 | + }, | ||
| 1541 | + "node_modules/cssesc": { | ||
| 1542 | + "version": "3.0.0", | ||
| 1543 | + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", | ||
| 1544 | + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", | ||
| 1545 | + "dev": true, | ||
| 1546 | + "license": "MIT", | ||
| 1547 | + "bin": { | ||
| 1548 | + "cssesc": "bin/cssesc" | ||
| 1549 | + }, | ||
| 1550 | + "engines": { | ||
| 1551 | + "node": ">=4" | ||
| 1552 | + } | ||
| 1553 | + }, | ||
| 1554 | + "node_modules/csstype": { | ||
| 1555 | + "version": "3.2.3", | ||
| 1556 | + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", | ||
| 1557 | + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", | ||
| 1558 | + "dev": true, | ||
| 1559 | + "license": "MIT" | ||
| 1560 | + }, | ||
| 1561 | + "node_modules/debug": { | ||
| 1562 | + "version": "4.4.3", | ||
| 1563 | + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", | ||
| 1564 | + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", | ||
| 1565 | + "dev": true, | ||
| 1566 | + "license": "MIT", | ||
| 1567 | + "dependencies": { | ||
| 1568 | + "ms": "^2.1.3" | ||
| 1569 | + }, | ||
| 1570 | + "engines": { | ||
| 1571 | + "node": ">=6.0" | ||
| 1572 | + }, | ||
| 1573 | + "peerDependenciesMeta": { | ||
| 1574 | + "supports-color": { | ||
| 1575 | + "optional": true | ||
| 1576 | + } | ||
| 1577 | + } | ||
| 1578 | + }, | ||
| 1579 | + "node_modules/didyoumean": { | ||
| 1580 | + "version": "1.2.2", | ||
| 1581 | + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", | ||
| 1582 | + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", | ||
| 1583 | + "dev": true, | ||
| 1584 | + "license": "Apache-2.0" | ||
| 1585 | + }, | ||
| 1586 | + "node_modules/dlv": { | ||
| 1587 | + "version": "1.1.3", | ||
| 1588 | + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", | ||
| 1589 | + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", | ||
| 1590 | + "dev": true, | ||
| 1591 | + "license": "MIT" | ||
| 1592 | + }, | ||
| 1593 | + "node_modules/electron-to-chromium": { | ||
| 1594 | + "version": "1.5.334", | ||
| 1595 | + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", | ||
| 1596 | + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", | ||
| 1597 | + "dev": true, | ||
| 1598 | + "license": "ISC" | ||
| 1599 | + }, | ||
| 1600 | + "node_modules/esbuild": { | ||
| 1601 | + "version": "0.21.5", | ||
| 1602 | + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", | ||
| 1603 | + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", | ||
| 1604 | + "dev": true, | ||
| 1605 | + "hasInstallScript": true, | ||
| 1606 | + "license": "MIT", | ||
| 1607 | + "bin": { | ||
| 1608 | + "esbuild": "bin/esbuild" | ||
| 1609 | + }, | ||
| 1610 | + "engines": { | ||
| 1611 | + "node": ">=12" | ||
| 1612 | + }, | ||
| 1613 | + "optionalDependencies": { | ||
| 1614 | + "@esbuild/aix-ppc64": "0.21.5", | ||
| 1615 | + "@esbuild/android-arm": "0.21.5", | ||
| 1616 | + "@esbuild/android-arm64": "0.21.5", | ||
| 1617 | + "@esbuild/android-x64": "0.21.5", | ||
| 1618 | + "@esbuild/darwin-arm64": "0.21.5", | ||
| 1619 | + "@esbuild/darwin-x64": "0.21.5", | ||
| 1620 | + "@esbuild/freebsd-arm64": "0.21.5", | ||
| 1621 | + "@esbuild/freebsd-x64": "0.21.5", | ||
| 1622 | + "@esbuild/linux-arm": "0.21.5", | ||
| 1623 | + "@esbuild/linux-arm64": "0.21.5", | ||
| 1624 | + "@esbuild/linux-ia32": "0.21.5", | ||
| 1625 | + "@esbuild/linux-loong64": "0.21.5", | ||
| 1626 | + "@esbuild/linux-mips64el": "0.21.5", | ||
| 1627 | + "@esbuild/linux-ppc64": "0.21.5", | ||
| 1628 | + "@esbuild/linux-riscv64": "0.21.5", | ||
| 1629 | + "@esbuild/linux-s390x": "0.21.5", | ||
| 1630 | + "@esbuild/linux-x64": "0.21.5", | ||
| 1631 | + "@esbuild/netbsd-x64": "0.21.5", | ||
| 1632 | + "@esbuild/openbsd-x64": "0.21.5", | ||
| 1633 | + "@esbuild/sunos-x64": "0.21.5", | ||
| 1634 | + "@esbuild/win32-arm64": "0.21.5", | ||
| 1635 | + "@esbuild/win32-ia32": "0.21.5", | ||
| 1636 | + "@esbuild/win32-x64": "0.21.5" | ||
| 1637 | + } | ||
| 1638 | + }, | ||
| 1639 | + "node_modules/escalade": { | ||
| 1640 | + "version": "3.2.0", | ||
| 1641 | + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", | ||
| 1642 | + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", | ||
| 1643 | + "dev": true, | ||
| 1644 | + "license": "MIT", | ||
| 1645 | + "engines": { | ||
| 1646 | + "node": ">=6" | ||
| 1647 | + } | ||
| 1648 | + }, | ||
| 1649 | + "node_modules/fast-glob": { | ||
| 1650 | + "version": "3.3.3", | ||
| 1651 | + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", | ||
| 1652 | + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", | ||
| 1653 | + "dev": true, | ||
| 1654 | + "license": "MIT", | ||
| 1655 | + "dependencies": { | ||
| 1656 | + "@nodelib/fs.stat": "^2.0.2", | ||
| 1657 | + "@nodelib/fs.walk": "^1.2.3", | ||
| 1658 | + "glob-parent": "^5.1.2", | ||
| 1659 | + "merge2": "^1.3.0", | ||
| 1660 | + "micromatch": "^4.0.8" | ||
| 1661 | + }, | ||
| 1662 | + "engines": { | ||
| 1663 | + "node": ">=8.6.0" | ||
| 1664 | + } | ||
| 1665 | + }, | ||
| 1666 | + "node_modules/fast-glob/node_modules/glob-parent": { | ||
| 1667 | + "version": "5.1.2", | ||
| 1668 | + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", | ||
| 1669 | + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", | ||
| 1670 | + "dev": true, | ||
| 1671 | + "license": "ISC", | ||
| 1672 | + "dependencies": { | ||
| 1673 | + "is-glob": "^4.0.1" | ||
| 1674 | + }, | ||
| 1675 | + "engines": { | ||
| 1676 | + "node": ">= 6" | ||
| 1677 | + } | ||
| 1678 | + }, | ||
| 1679 | + "node_modules/fastq": { | ||
| 1680 | + "version": "1.20.1", | ||
| 1681 | + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", | ||
| 1682 | + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", | ||
| 1683 | + "dev": true, | ||
| 1684 | + "license": "ISC", | ||
| 1685 | + "dependencies": { | ||
| 1686 | + "reusify": "^1.0.4" | ||
| 1687 | + } | ||
| 1688 | + }, | ||
| 1689 | + "node_modules/fill-range": { | ||
| 1690 | + "version": "7.1.1", | ||
| 1691 | + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", | ||
| 1692 | + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", | ||
| 1693 | + "dev": true, | ||
| 1694 | + "license": "MIT", | ||
| 1695 | + "dependencies": { | ||
| 1696 | + "to-regex-range": "^5.0.1" | ||
| 1697 | + }, | ||
| 1698 | + "engines": { | ||
| 1699 | + "node": ">=8" | ||
| 1700 | + } | ||
| 1701 | + }, | ||
| 1702 | + "node_modules/fraction.js": { | ||
| 1703 | + "version": "5.3.4", | ||
| 1704 | + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", | ||
| 1705 | + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", | ||
| 1706 | + "dev": true, | ||
| 1707 | + "license": "MIT", | ||
| 1708 | + "engines": { | ||
| 1709 | + "node": "*" | ||
| 1710 | + }, | ||
| 1711 | + "funding": { | ||
| 1712 | + "type": "github", | ||
| 1713 | + "url": "https://github.com/sponsors/rawify" | ||
| 1714 | + } | ||
| 1715 | + }, | ||
| 1716 | + "node_modules/fsevents": { | ||
| 1717 | + "version": "2.3.3", | ||
| 1718 | + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", | ||
| 1719 | + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", | ||
| 1720 | + "dev": true, | ||
| 1721 | + "hasInstallScript": true, | ||
| 1722 | + "license": "MIT", | ||
| 1723 | + "optional": true, | ||
| 1724 | + "os": [ | ||
| 1725 | + "darwin" | ||
| 1726 | + ], | ||
| 1727 | + "engines": { | ||
| 1728 | + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" | ||
| 1729 | + } | ||
| 1730 | + }, | ||
| 1731 | + "node_modules/function-bind": { | ||
| 1732 | + "version": "1.1.2", | ||
| 1733 | + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", | ||
| 1734 | + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", | ||
| 1735 | + "dev": true, | ||
| 1736 | + "license": "MIT", | ||
| 1737 | + "funding": { | ||
| 1738 | + "url": "https://github.com/sponsors/ljharb" | ||
| 1739 | + } | ||
| 1740 | + }, | ||
| 1741 | + "node_modules/gensync": { | ||
| 1742 | + "version": "1.0.0-beta.2", | ||
| 1743 | + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", | ||
| 1744 | + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", | ||
| 1745 | + "dev": true, | ||
| 1746 | + "license": "MIT", | ||
| 1747 | + "engines": { | ||
| 1748 | + "node": ">=6.9.0" | ||
| 1749 | + } | ||
| 1750 | + }, | ||
| 1751 | + "node_modules/glob-parent": { | ||
| 1752 | + "version": "6.0.2", | ||
| 1753 | + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", | ||
| 1754 | + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", | ||
| 1755 | + "dev": true, | ||
| 1756 | + "license": "ISC", | ||
| 1757 | + "dependencies": { | ||
| 1758 | + "is-glob": "^4.0.3" | ||
| 1759 | + }, | ||
| 1760 | + "engines": { | ||
| 1761 | + "node": ">=10.13.0" | ||
| 1762 | + } | ||
| 1763 | + }, | ||
| 1764 | + "node_modules/hasown": { | ||
| 1765 | + "version": "2.0.2", | ||
| 1766 | + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", | ||
| 1767 | + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", | ||
| 1768 | + "dev": true, | ||
| 1769 | + "license": "MIT", | ||
| 1770 | + "dependencies": { | ||
| 1771 | + "function-bind": "^1.1.2" | ||
| 1772 | + }, | ||
| 1773 | + "engines": { | ||
| 1774 | + "node": ">= 0.4" | ||
| 1775 | + } | ||
| 1776 | + }, | ||
| 1777 | + "node_modules/is-binary-path": { | ||
| 1778 | + "version": "2.1.0", | ||
| 1779 | + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", | ||
| 1780 | + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", | ||
| 1781 | + "dev": true, | ||
| 1782 | + "license": "MIT", | ||
| 1783 | + "dependencies": { | ||
| 1784 | + "binary-extensions": "^2.0.0" | ||
| 1785 | + }, | ||
| 1786 | + "engines": { | ||
| 1787 | + "node": ">=8" | ||
| 1788 | + } | ||
| 1789 | + }, | ||
| 1790 | + "node_modules/is-core-module": { | ||
| 1791 | + "version": "2.16.1", | ||
| 1792 | + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", | ||
| 1793 | + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", | ||
| 1794 | + "dev": true, | ||
| 1795 | + "license": "MIT", | ||
| 1796 | + "dependencies": { | ||
| 1797 | + "hasown": "^2.0.2" | ||
| 1798 | + }, | ||
| 1799 | + "engines": { | ||
| 1800 | + "node": ">= 0.4" | ||
| 1801 | + }, | ||
| 1802 | + "funding": { | ||
| 1803 | + "url": "https://github.com/sponsors/ljharb" | ||
| 1804 | + } | ||
| 1805 | + }, | ||
| 1806 | + "node_modules/is-extglob": { | ||
| 1807 | + "version": "2.1.1", | ||
| 1808 | + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", | ||
| 1809 | + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", | ||
| 1810 | + "dev": true, | ||
| 1811 | + "license": "MIT", | ||
| 1812 | + "engines": { | ||
| 1813 | + "node": ">=0.10.0" | ||
| 1814 | + } | ||
| 1815 | + }, | ||
| 1816 | + "node_modules/is-glob": { | ||
| 1817 | + "version": "4.0.3", | ||
| 1818 | + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", | ||
| 1819 | + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", | ||
| 1820 | + "dev": true, | ||
| 1821 | + "license": "MIT", | ||
| 1822 | + "dependencies": { | ||
| 1823 | + "is-extglob": "^2.1.1" | ||
| 1824 | + }, | ||
| 1825 | + "engines": { | ||
| 1826 | + "node": ">=0.10.0" | ||
| 1827 | + } | ||
| 1828 | + }, | ||
| 1829 | + "node_modules/is-number": { | ||
| 1830 | + "version": "7.0.0", | ||
| 1831 | + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", | ||
| 1832 | + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", | ||
| 1833 | + "dev": true, | ||
| 1834 | + "license": "MIT", | ||
| 1835 | + "engines": { | ||
| 1836 | + "node": ">=0.12.0" | ||
| 1837 | + } | ||
| 1838 | + }, | ||
| 1839 | + "node_modules/jiti": { | ||
| 1840 | + "version": "1.21.7", | ||
| 1841 | + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", | ||
| 1842 | + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", | ||
| 1843 | + "dev": true, | ||
| 1844 | + "license": "MIT", | ||
| 1845 | + "bin": { | ||
| 1846 | + "jiti": "bin/jiti.js" | ||
| 1847 | + } | ||
| 1848 | + }, | ||
| 1849 | + "node_modules/js-tokens": { | ||
| 1850 | + "version": "4.0.0", | ||
| 1851 | + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", | ||
| 1852 | + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", | ||
| 1853 | + "license": "MIT" | ||
| 1854 | + }, | ||
| 1855 | + "node_modules/jsesc": { | ||
| 1856 | + "version": "3.1.0", | ||
| 1857 | + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", | ||
| 1858 | + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", | ||
| 1859 | + "dev": true, | ||
| 1860 | + "license": "MIT", | ||
| 1861 | + "bin": { | ||
| 1862 | + "jsesc": "bin/jsesc" | ||
| 1863 | + }, | ||
| 1864 | + "engines": { | ||
| 1865 | + "node": ">=6" | ||
| 1866 | + } | ||
| 1867 | + }, | ||
| 1868 | + "node_modules/json5": { | ||
| 1869 | + "version": "2.2.3", | ||
| 1870 | + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", | ||
| 1871 | + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", | ||
| 1872 | + "dev": true, | ||
| 1873 | + "license": "MIT", | ||
| 1874 | + "bin": { | ||
| 1875 | + "json5": "lib/cli.js" | ||
| 1876 | + }, | ||
| 1877 | + "engines": { | ||
| 1878 | + "node": ">=6" | ||
| 1879 | + } | ||
| 1880 | + }, | ||
| 1881 | + "node_modules/lilconfig": { | ||
| 1882 | + "version": "3.1.3", | ||
| 1883 | + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", | ||
| 1884 | + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", | ||
| 1885 | + "dev": true, | ||
| 1886 | + "license": "MIT", | ||
| 1887 | + "engines": { | ||
| 1888 | + "node": ">=14" | ||
| 1889 | + }, | ||
| 1890 | + "funding": { | ||
| 1891 | + "url": "https://github.com/sponsors/antonk52" | ||
| 1892 | + } | ||
| 1893 | + }, | ||
| 1894 | + "node_modules/lines-and-columns": { | ||
| 1895 | + "version": "1.2.4", | ||
| 1896 | + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", | ||
| 1897 | + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", | ||
| 1898 | + "dev": true, | ||
| 1899 | + "license": "MIT" | ||
| 1900 | + }, | ||
| 1901 | + "node_modules/loose-envify": { | ||
| 1902 | + "version": "1.4.0", | ||
| 1903 | + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", | ||
| 1904 | + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", | ||
| 1905 | + "license": "MIT", | ||
| 1906 | + "dependencies": { | ||
| 1907 | + "js-tokens": "^3.0.0 || ^4.0.0" | ||
| 1908 | + }, | ||
| 1909 | + "bin": { | ||
| 1910 | + "loose-envify": "cli.js" | ||
| 1911 | + } | ||
| 1912 | + }, | ||
| 1913 | + "node_modules/lru-cache": { | ||
| 1914 | + "version": "5.1.1", | ||
| 1915 | + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", | ||
| 1916 | + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", | ||
| 1917 | + "dev": true, | ||
| 1918 | + "license": "ISC", | ||
| 1919 | + "dependencies": { | ||
| 1920 | + "yallist": "^3.0.2" | ||
| 1921 | + } | ||
| 1922 | + }, | ||
| 1923 | + "node_modules/merge2": { | ||
| 1924 | + "version": "1.4.1", | ||
| 1925 | + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", | ||
| 1926 | + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", | ||
| 1927 | + "dev": true, | ||
| 1928 | + "license": "MIT", | ||
| 1929 | + "engines": { | ||
| 1930 | + "node": ">= 8" | ||
| 1931 | + } | ||
| 1932 | + }, | ||
| 1933 | + "node_modules/micromatch": { | ||
| 1934 | + "version": "4.0.8", | ||
| 1935 | + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", | ||
| 1936 | + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", | ||
| 1937 | + "dev": true, | ||
| 1938 | + "license": "MIT", | ||
| 1939 | + "dependencies": { | ||
| 1940 | + "braces": "^3.0.3", | ||
| 1941 | + "picomatch": "^2.3.1" | ||
| 1942 | + }, | ||
| 1943 | + "engines": { | ||
| 1944 | + "node": ">=8.6" | ||
| 1945 | + } | ||
| 1946 | + }, | ||
| 1947 | + "node_modules/ms": { | ||
| 1948 | + "version": "2.1.3", | ||
| 1949 | + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", | ||
| 1950 | + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", | ||
| 1951 | + "dev": true, | ||
| 1952 | + "license": "MIT" | ||
| 1953 | + }, | ||
| 1954 | + "node_modules/mz": { | ||
| 1955 | + "version": "2.7.0", | ||
| 1956 | + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", | ||
| 1957 | + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", | ||
| 1958 | + "dev": true, | ||
| 1959 | + "license": "MIT", | ||
| 1960 | + "dependencies": { | ||
| 1961 | + "any-promise": "^1.0.0", | ||
| 1962 | + "object-assign": "^4.0.1", | ||
| 1963 | + "thenify-all": "^1.0.0" | ||
| 1964 | + } | ||
| 1965 | + }, | ||
| 1966 | + "node_modules/nanoid": { | ||
| 1967 | + "version": "3.3.11", | ||
| 1968 | + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", | ||
| 1969 | + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", | ||
| 1970 | + "dev": true, | ||
| 1971 | + "funding": [ | ||
| 1972 | + { | ||
| 1973 | + "type": "github", | ||
| 1974 | + "url": "https://github.com/sponsors/ai" | ||
| 1975 | + } | ||
| 1976 | + ], | ||
| 1977 | + "license": "MIT", | ||
| 1978 | + "bin": { | ||
| 1979 | + "nanoid": "bin/nanoid.cjs" | ||
| 1980 | + }, | ||
| 1981 | + "engines": { | ||
| 1982 | + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" | ||
| 1983 | + } | ||
| 1984 | + }, | ||
| 1985 | + "node_modules/node-releases": { | ||
| 1986 | + "version": "2.0.37", | ||
| 1987 | + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", | ||
| 1988 | + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", | ||
| 1989 | + "dev": true, | ||
| 1990 | + "license": "MIT" | ||
| 1991 | + }, | ||
| 1992 | + "node_modules/normalize-path": { | ||
| 1993 | + "version": "3.0.0", | ||
| 1994 | + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", | ||
| 1995 | + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", | ||
| 1996 | + "dev": true, | ||
| 1997 | + "license": "MIT", | ||
| 1998 | + "engines": { | ||
| 1999 | + "node": ">=0.10.0" | ||
| 2000 | + } | ||
| 2001 | + }, | ||
| 2002 | + "node_modules/object-assign": { | ||
| 2003 | + "version": "4.1.1", | ||
| 2004 | + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", | ||
| 2005 | + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", | ||
| 2006 | + "dev": true, | ||
| 2007 | + "license": "MIT", | ||
| 2008 | + "engines": { | ||
| 2009 | + "node": ">=0.10.0" | ||
| 2010 | + } | ||
| 2011 | + }, | ||
| 2012 | + "node_modules/object-hash": { | ||
| 2013 | + "version": "3.0.0", | ||
| 2014 | + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", | ||
| 2015 | + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", | ||
| 2016 | + "dev": true, | ||
| 2017 | + "license": "MIT", | ||
| 2018 | + "engines": { | ||
| 2019 | + "node": ">= 6" | ||
| 2020 | + } | ||
| 2021 | + }, | ||
| 2022 | + "node_modules/path-parse": { | ||
| 2023 | + "version": "1.0.7", | ||
| 2024 | + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", | ||
| 2025 | + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", | ||
| 2026 | + "dev": true, | ||
| 2027 | + "license": "MIT" | ||
| 2028 | + }, | ||
| 2029 | + "node_modules/picocolors": { | ||
| 2030 | + "version": "1.1.1", | ||
| 2031 | + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", | ||
| 2032 | + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", | ||
| 2033 | + "dev": true, | ||
| 2034 | + "license": "ISC" | ||
| 2035 | + }, | ||
| 2036 | + "node_modules/picomatch": { | ||
| 2037 | + "version": "2.3.2", | ||
| 2038 | + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", | ||
| 2039 | + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", | ||
| 2040 | + "dev": true, | ||
| 2041 | + "license": "MIT", | ||
| 2042 | + "engines": { | ||
| 2043 | + "node": ">=8.6" | ||
| 2044 | + }, | ||
| 2045 | + "funding": { | ||
| 2046 | + "url": "https://github.com/sponsors/jonschlinkert" | ||
| 2047 | + } | ||
| 2048 | + }, | ||
| 2049 | + "node_modules/pify": { | ||
| 2050 | + "version": "2.3.0", | ||
| 2051 | + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", | ||
| 2052 | + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", | ||
| 2053 | + "dev": true, | ||
| 2054 | + "license": "MIT", | ||
| 2055 | + "engines": { | ||
| 2056 | + "node": ">=0.10.0" | ||
| 2057 | + } | ||
| 2058 | + }, | ||
| 2059 | + "node_modules/pirates": { | ||
| 2060 | + "version": "4.0.7", | ||
| 2061 | + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", | ||
| 2062 | + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", | ||
| 2063 | + "dev": true, | ||
| 2064 | + "license": "MIT", | ||
| 2065 | + "engines": { | ||
| 2066 | + "node": ">= 6" | ||
| 2067 | + } | ||
| 2068 | + }, | ||
| 2069 | + "node_modules/postcss": { | ||
| 2070 | + "version": "8.5.9", | ||
| 2071 | + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", | ||
| 2072 | + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", | ||
| 2073 | + "dev": true, | ||
| 2074 | + "funding": [ | ||
| 2075 | + { | ||
| 2076 | + "type": "opencollective", | ||
| 2077 | + "url": "https://opencollective.com/postcss/" | ||
| 2078 | + }, | ||
| 2079 | + { | ||
| 2080 | + "type": "tidelift", | ||
| 2081 | + "url": "https://tidelift.com/funding/github/npm/postcss" | ||
| 2082 | + }, | ||
| 2083 | + { | ||
| 2084 | + "type": "github", | ||
| 2085 | + "url": "https://github.com/sponsors/ai" | ||
| 2086 | + } | ||
| 2087 | + ], | ||
| 2088 | + "license": "MIT", | ||
| 2089 | + "dependencies": { | ||
| 2090 | + "nanoid": "^3.3.11", | ||
| 2091 | + "picocolors": "^1.1.1", | ||
| 2092 | + "source-map-js": "^1.2.1" | ||
| 2093 | + }, | ||
| 2094 | + "engines": { | ||
| 2095 | + "node": "^10 || ^12 || >=14" | ||
| 2096 | + } | ||
| 2097 | + }, | ||
| 2098 | + "node_modules/postcss-import": { | ||
| 2099 | + "version": "15.1.0", | ||
| 2100 | + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", | ||
| 2101 | + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", | ||
| 2102 | + "dev": true, | ||
| 2103 | + "license": "MIT", | ||
| 2104 | + "dependencies": { | ||
| 2105 | + "postcss-value-parser": "^4.0.0", | ||
| 2106 | + "read-cache": "^1.0.0", | ||
| 2107 | + "resolve": "^1.1.7" | ||
| 2108 | + }, | ||
| 2109 | + "engines": { | ||
| 2110 | + "node": ">=14.0.0" | ||
| 2111 | + }, | ||
| 2112 | + "peerDependencies": { | ||
| 2113 | + "postcss": "^8.0.0" | ||
| 2114 | + } | ||
| 2115 | + }, | ||
| 2116 | + "node_modules/postcss-js": { | ||
| 2117 | + "version": "4.1.0", | ||
| 2118 | + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", | ||
| 2119 | + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", | ||
| 2120 | + "dev": true, | ||
| 2121 | + "funding": [ | ||
| 2122 | + { | ||
| 2123 | + "type": "opencollective", | ||
| 2124 | + "url": "https://opencollective.com/postcss/" | ||
| 2125 | + }, | ||
| 2126 | + { | ||
| 2127 | + "type": "github", | ||
| 2128 | + "url": "https://github.com/sponsors/ai" | ||
| 2129 | + } | ||
| 2130 | + ], | ||
| 2131 | + "license": "MIT", | ||
| 2132 | + "dependencies": { | ||
| 2133 | + "camelcase-css": "^2.0.1" | ||
| 2134 | + }, | ||
| 2135 | + "engines": { | ||
| 2136 | + "node": "^12 || ^14 || >= 16" | ||
| 2137 | + }, | ||
| 2138 | + "peerDependencies": { | ||
| 2139 | + "postcss": "^8.4.21" | ||
| 2140 | + } | ||
| 2141 | + }, | ||
| 2142 | + "node_modules/postcss-load-config": { | ||
| 2143 | + "version": "6.0.1", | ||
| 2144 | + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", | ||
| 2145 | + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", | ||
| 2146 | + "dev": true, | ||
| 2147 | + "funding": [ | ||
| 2148 | + { | ||
| 2149 | + "type": "opencollective", | ||
| 2150 | + "url": "https://opencollective.com/postcss/" | ||
| 2151 | + }, | ||
| 2152 | + { | ||
| 2153 | + "type": "github", | ||
| 2154 | + "url": "https://github.com/sponsors/ai" | ||
| 2155 | + } | ||
| 2156 | + ], | ||
| 2157 | + "license": "MIT", | ||
| 2158 | + "dependencies": { | ||
| 2159 | + "lilconfig": "^3.1.1" | ||
| 2160 | + }, | ||
| 2161 | + "engines": { | ||
| 2162 | + "node": ">= 18" | ||
| 2163 | + }, | ||
| 2164 | + "peerDependencies": { | ||
| 2165 | + "jiti": ">=1.21.0", | ||
| 2166 | + "postcss": ">=8.0.9", | ||
| 2167 | + "tsx": "^4.8.1", | ||
| 2168 | + "yaml": "^2.4.2" | ||
| 2169 | + }, | ||
| 2170 | + "peerDependenciesMeta": { | ||
| 2171 | + "jiti": { | ||
| 2172 | + "optional": true | ||
| 2173 | + }, | ||
| 2174 | + "postcss": { | ||
| 2175 | + "optional": true | ||
| 2176 | + }, | ||
| 2177 | + "tsx": { | ||
| 2178 | + "optional": true | ||
| 2179 | + }, | ||
| 2180 | + "yaml": { | ||
| 2181 | + "optional": true | ||
| 2182 | + } | ||
| 2183 | + } | ||
| 2184 | + }, | ||
| 2185 | + "node_modules/postcss-nested": { | ||
| 2186 | + "version": "6.2.0", | ||
| 2187 | + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", | ||
| 2188 | + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", | ||
| 2189 | + "dev": true, | ||
| 2190 | + "funding": [ | ||
| 2191 | + { | ||
| 2192 | + "type": "opencollective", | ||
| 2193 | + "url": "https://opencollective.com/postcss/" | ||
| 2194 | + }, | ||
| 2195 | + { | ||
| 2196 | + "type": "github", | ||
| 2197 | + "url": "https://github.com/sponsors/ai" | ||
| 2198 | + } | ||
| 2199 | + ], | ||
| 2200 | + "license": "MIT", | ||
| 2201 | + "dependencies": { | ||
| 2202 | + "postcss-selector-parser": "^6.1.1" | ||
| 2203 | + }, | ||
| 2204 | + "engines": { | ||
| 2205 | + "node": ">=12.0" | ||
| 2206 | + }, | ||
| 2207 | + "peerDependencies": { | ||
| 2208 | + "postcss": "^8.2.14" | ||
| 2209 | + } | ||
| 2210 | + }, | ||
| 2211 | + "node_modules/postcss-selector-parser": { | ||
| 2212 | + "version": "6.1.2", | ||
| 2213 | + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", | ||
| 2214 | + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", | ||
| 2215 | + "dev": true, | ||
| 2216 | + "license": "MIT", | ||
| 2217 | + "dependencies": { | ||
| 2218 | + "cssesc": "^3.0.0", | ||
| 2219 | + "util-deprecate": "^1.0.2" | ||
| 2220 | + }, | ||
| 2221 | + "engines": { | ||
| 2222 | + "node": ">=4" | ||
| 2223 | + } | ||
| 2224 | + }, | ||
| 2225 | + "node_modules/postcss-value-parser": { | ||
| 2226 | + "version": "4.2.0", | ||
| 2227 | + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", | ||
| 2228 | + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", | ||
| 2229 | + "dev": true, | ||
| 2230 | + "license": "MIT" | ||
| 2231 | + }, | ||
| 2232 | + "node_modules/queue-microtask": { | ||
| 2233 | + "version": "1.2.3", | ||
| 2234 | + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", | ||
| 2235 | + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", | ||
| 2236 | + "dev": true, | ||
| 2237 | + "funding": [ | ||
| 2238 | + { | ||
| 2239 | + "type": "github", | ||
| 2240 | + "url": "https://github.com/sponsors/feross" | ||
| 2241 | + }, | ||
| 2242 | + { | ||
| 2243 | + "type": "patreon", | ||
| 2244 | + "url": "https://www.patreon.com/feross" | ||
| 2245 | + }, | ||
| 2246 | + { | ||
| 2247 | + "type": "consulting", | ||
| 2248 | + "url": "https://feross.org/support" | ||
| 2249 | + } | ||
| 2250 | + ], | ||
| 2251 | + "license": "MIT" | ||
| 2252 | + }, | ||
| 2253 | + "node_modules/react": { | ||
| 2254 | + "version": "18.3.1", | ||
| 2255 | + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", | ||
| 2256 | + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", | ||
| 2257 | + "license": "MIT", | ||
| 2258 | + "dependencies": { | ||
| 2259 | + "loose-envify": "^1.1.0" | ||
| 2260 | + }, | ||
| 2261 | + "engines": { | ||
| 2262 | + "node": ">=0.10.0" | ||
| 2263 | + } | ||
| 2264 | + }, | ||
| 2265 | + "node_modules/react-dom": { | ||
| 2266 | + "version": "18.3.1", | ||
| 2267 | + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", | ||
| 2268 | + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", | ||
| 2269 | + "license": "MIT", | ||
| 2270 | + "dependencies": { | ||
| 2271 | + "loose-envify": "^1.1.0", | ||
| 2272 | + "scheduler": "^0.23.2" | ||
| 2273 | + }, | ||
| 2274 | + "peerDependencies": { | ||
| 2275 | + "react": "^18.3.1" | ||
| 2276 | + } | ||
| 2277 | + }, | ||
| 2278 | + "node_modules/react-refresh": { | ||
| 2279 | + "version": "0.17.0", | ||
| 2280 | + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", | ||
| 2281 | + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", | ||
| 2282 | + "dev": true, | ||
| 2283 | + "license": "MIT", | ||
| 2284 | + "engines": { | ||
| 2285 | + "node": ">=0.10.0" | ||
| 2286 | + } | ||
| 2287 | + }, | ||
| 2288 | + "node_modules/react-router": { | ||
| 2289 | + "version": "6.30.3", | ||
| 2290 | + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", | ||
| 2291 | + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", | ||
| 2292 | + "license": "MIT", | ||
| 2293 | + "dependencies": { | ||
| 2294 | + "@remix-run/router": "1.23.2" | ||
| 2295 | + }, | ||
| 2296 | + "engines": { | ||
| 2297 | + "node": ">=14.0.0" | ||
| 2298 | + }, | ||
| 2299 | + "peerDependencies": { | ||
| 2300 | + "react": ">=16.8" | ||
| 2301 | + } | ||
| 2302 | + }, | ||
| 2303 | + "node_modules/react-router-dom": { | ||
| 2304 | + "version": "6.30.3", | ||
| 2305 | + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", | ||
| 2306 | + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", | ||
| 2307 | + "license": "MIT", | ||
| 2308 | + "dependencies": { | ||
| 2309 | + "@remix-run/router": "1.23.2", | ||
| 2310 | + "react-router": "6.30.3" | ||
| 2311 | + }, | ||
| 2312 | + "engines": { | ||
| 2313 | + "node": ">=14.0.0" | ||
| 2314 | + }, | ||
| 2315 | + "peerDependencies": { | ||
| 2316 | + "react": ">=16.8", | ||
| 2317 | + "react-dom": ">=16.8" | ||
| 2318 | + } | ||
| 2319 | + }, | ||
| 2320 | + "node_modules/read-cache": { | ||
| 2321 | + "version": "1.0.0", | ||
| 2322 | + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", | ||
| 2323 | + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", | ||
| 2324 | + "dev": true, | ||
| 2325 | + "license": "MIT", | ||
| 2326 | + "dependencies": { | ||
| 2327 | + "pify": "^2.3.0" | ||
| 2328 | + } | ||
| 2329 | + }, | ||
| 2330 | + "node_modules/readdirp": { | ||
| 2331 | + "version": "3.6.0", | ||
| 2332 | + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", | ||
| 2333 | + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", | ||
| 2334 | + "dev": true, | ||
| 2335 | + "license": "MIT", | ||
| 2336 | + "dependencies": { | ||
| 2337 | + "picomatch": "^2.2.1" | ||
| 2338 | + }, | ||
| 2339 | + "engines": { | ||
| 2340 | + "node": ">=8.10.0" | ||
| 2341 | + } | ||
| 2342 | + }, | ||
| 2343 | + "node_modules/resolve": { | ||
| 2344 | + "version": "1.22.11", | ||
| 2345 | + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", | ||
| 2346 | + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", | ||
| 2347 | + "dev": true, | ||
| 2348 | + "license": "MIT", | ||
| 2349 | + "dependencies": { | ||
| 2350 | + "is-core-module": "^2.16.1", | ||
| 2351 | + "path-parse": "^1.0.7", | ||
| 2352 | + "supports-preserve-symlinks-flag": "^1.0.0" | ||
| 2353 | + }, | ||
| 2354 | + "bin": { | ||
| 2355 | + "resolve": "bin/resolve" | ||
| 2356 | + }, | ||
| 2357 | + "engines": { | ||
| 2358 | + "node": ">= 0.4" | ||
| 2359 | + }, | ||
| 2360 | + "funding": { | ||
| 2361 | + "url": "https://github.com/sponsors/ljharb" | ||
| 2362 | + } | ||
| 2363 | + }, | ||
| 2364 | + "node_modules/reusify": { | ||
| 2365 | + "version": "1.1.0", | ||
| 2366 | + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", | ||
| 2367 | + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", | ||
| 2368 | + "dev": true, | ||
| 2369 | + "license": "MIT", | ||
| 2370 | + "engines": { | ||
| 2371 | + "iojs": ">=1.0.0", | ||
| 2372 | + "node": ">=0.10.0" | ||
| 2373 | + } | ||
| 2374 | + }, | ||
| 2375 | + "node_modules/rollup": { | ||
| 2376 | + "version": "4.60.1", | ||
| 2377 | + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", | ||
| 2378 | + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", | ||
| 2379 | + "dev": true, | ||
| 2380 | + "license": "MIT", | ||
| 2381 | + "dependencies": { | ||
| 2382 | + "@types/estree": "1.0.8" | ||
| 2383 | + }, | ||
| 2384 | + "bin": { | ||
| 2385 | + "rollup": "dist/bin/rollup" | ||
| 2386 | + }, | ||
| 2387 | + "engines": { | ||
| 2388 | + "node": ">=18.0.0", | ||
| 2389 | + "npm": ">=8.0.0" | ||
| 2390 | + }, | ||
| 2391 | + "optionalDependencies": { | ||
| 2392 | + "@rollup/rollup-android-arm-eabi": "4.60.1", | ||
| 2393 | + "@rollup/rollup-android-arm64": "4.60.1", | ||
| 2394 | + "@rollup/rollup-darwin-arm64": "4.60.1", | ||
| 2395 | + "@rollup/rollup-darwin-x64": "4.60.1", | ||
| 2396 | + "@rollup/rollup-freebsd-arm64": "4.60.1", | ||
| 2397 | + "@rollup/rollup-freebsd-x64": "4.60.1", | ||
| 2398 | + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", | ||
| 2399 | + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", | ||
| 2400 | + "@rollup/rollup-linux-arm64-gnu": "4.60.1", | ||
| 2401 | + "@rollup/rollup-linux-arm64-musl": "4.60.1", | ||
| 2402 | + "@rollup/rollup-linux-loong64-gnu": "4.60.1", | ||
| 2403 | + "@rollup/rollup-linux-loong64-musl": "4.60.1", | ||
| 2404 | + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", | ||
| 2405 | + "@rollup/rollup-linux-ppc64-musl": "4.60.1", | ||
| 2406 | + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", | ||
| 2407 | + "@rollup/rollup-linux-riscv64-musl": "4.60.1", | ||
| 2408 | + "@rollup/rollup-linux-s390x-gnu": "4.60.1", | ||
| 2409 | + "@rollup/rollup-linux-x64-gnu": "4.60.1", | ||
| 2410 | + "@rollup/rollup-linux-x64-musl": "4.60.1", | ||
| 2411 | + "@rollup/rollup-openbsd-x64": "4.60.1", | ||
| 2412 | + "@rollup/rollup-openharmony-arm64": "4.60.1", | ||
| 2413 | + "@rollup/rollup-win32-arm64-msvc": "4.60.1", | ||
| 2414 | + "@rollup/rollup-win32-ia32-msvc": "4.60.1", | ||
| 2415 | + "@rollup/rollup-win32-x64-gnu": "4.60.1", | ||
| 2416 | + "@rollup/rollup-win32-x64-msvc": "4.60.1", | ||
| 2417 | + "fsevents": "~2.3.2" | ||
| 2418 | + } | ||
| 2419 | + }, | ||
| 2420 | + "node_modules/run-parallel": { | ||
| 2421 | + "version": "1.2.0", | ||
| 2422 | + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", | ||
| 2423 | + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", | ||
| 2424 | + "dev": true, | ||
| 2425 | + "funding": [ | ||
| 2426 | + { | ||
| 2427 | + "type": "github", | ||
| 2428 | + "url": "https://github.com/sponsors/feross" | ||
| 2429 | + }, | ||
| 2430 | + { | ||
| 2431 | + "type": "patreon", | ||
| 2432 | + "url": "https://www.patreon.com/feross" | ||
| 2433 | + }, | ||
| 2434 | + { | ||
| 2435 | + "type": "consulting", | ||
| 2436 | + "url": "https://feross.org/support" | ||
| 2437 | + } | ||
| 2438 | + ], | ||
| 2439 | + "license": "MIT", | ||
| 2440 | + "dependencies": { | ||
| 2441 | + "queue-microtask": "^1.2.2" | ||
| 2442 | + } | ||
| 2443 | + }, | ||
| 2444 | + "node_modules/scheduler": { | ||
| 2445 | + "version": "0.23.2", | ||
| 2446 | + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", | ||
| 2447 | + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", | ||
| 2448 | + "license": "MIT", | ||
| 2449 | + "dependencies": { | ||
| 2450 | + "loose-envify": "^1.1.0" | ||
| 2451 | + } | ||
| 2452 | + }, | ||
| 2453 | + "node_modules/semver": { | ||
| 2454 | + "version": "6.3.1", | ||
| 2455 | + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", | ||
| 2456 | + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", | ||
| 2457 | + "dev": true, | ||
| 2458 | + "license": "ISC", | ||
| 2459 | + "bin": { | ||
| 2460 | + "semver": "bin/semver.js" | ||
| 2461 | + } | ||
| 2462 | + }, | ||
| 2463 | + "node_modules/source-map-js": { | ||
| 2464 | + "version": "1.2.1", | ||
| 2465 | + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", | ||
| 2466 | + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", | ||
| 2467 | + "dev": true, | ||
| 2468 | + "license": "BSD-3-Clause", | ||
| 2469 | + "engines": { | ||
| 2470 | + "node": ">=0.10.0" | ||
| 2471 | + } | ||
| 2472 | + }, | ||
| 2473 | + "node_modules/sucrase": { | ||
| 2474 | + "version": "3.35.1", | ||
| 2475 | + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", | ||
| 2476 | + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", | ||
| 2477 | + "dev": true, | ||
| 2478 | + "license": "MIT", | ||
| 2479 | + "dependencies": { | ||
| 2480 | + "@jridgewell/gen-mapping": "^0.3.2", | ||
| 2481 | + "commander": "^4.0.0", | ||
| 2482 | + "lines-and-columns": "^1.1.6", | ||
| 2483 | + "mz": "^2.7.0", | ||
| 2484 | + "pirates": "^4.0.1", | ||
| 2485 | + "tinyglobby": "^0.2.11", | ||
| 2486 | + "ts-interface-checker": "^0.1.9" | ||
| 2487 | + }, | ||
| 2488 | + "bin": { | ||
| 2489 | + "sucrase": "bin/sucrase", | ||
| 2490 | + "sucrase-node": "bin/sucrase-node" | ||
| 2491 | + }, | ||
| 2492 | + "engines": { | ||
| 2493 | + "node": ">=16 || 14 >=14.17" | ||
| 2494 | + } | ||
| 2495 | + }, | ||
| 2496 | + "node_modules/supports-preserve-symlinks-flag": { | ||
| 2497 | + "version": "1.0.0", | ||
| 2498 | + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", | ||
| 2499 | + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", | ||
| 2500 | + "dev": true, | ||
| 2501 | + "license": "MIT", | ||
| 2502 | + "engines": { | ||
| 2503 | + "node": ">= 0.4" | ||
| 2504 | + }, | ||
| 2505 | + "funding": { | ||
| 2506 | + "url": "https://github.com/sponsors/ljharb" | ||
| 2507 | + } | ||
| 2508 | + }, | ||
| 2509 | + "node_modules/tailwindcss": { | ||
| 2510 | + "version": "3.4.19", | ||
| 2511 | + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", | ||
| 2512 | + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", | ||
| 2513 | + "dev": true, | ||
| 2514 | + "license": "MIT", | ||
| 2515 | + "dependencies": { | ||
| 2516 | + "@alloc/quick-lru": "^5.2.0", | ||
| 2517 | + "arg": "^5.0.2", | ||
| 2518 | + "chokidar": "^3.6.0", | ||
| 2519 | + "didyoumean": "^1.2.2", | ||
| 2520 | + "dlv": "^1.1.3", | ||
| 2521 | + "fast-glob": "^3.3.2", | ||
| 2522 | + "glob-parent": "^6.0.2", | ||
| 2523 | + "is-glob": "^4.0.3", | ||
| 2524 | + "jiti": "^1.21.7", | ||
| 2525 | + "lilconfig": "^3.1.3", | ||
| 2526 | + "micromatch": "^4.0.8", | ||
| 2527 | + "normalize-path": "^3.0.0", | ||
| 2528 | + "object-hash": "^3.0.0", | ||
| 2529 | + "picocolors": "^1.1.1", | ||
| 2530 | + "postcss": "^8.4.47", | ||
| 2531 | + "postcss-import": "^15.1.0", | ||
| 2532 | + "postcss-js": "^4.0.1", | ||
| 2533 | + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", | ||
| 2534 | + "postcss-nested": "^6.2.0", | ||
| 2535 | + "postcss-selector-parser": "^6.1.2", | ||
| 2536 | + "resolve": "^1.22.8", | ||
| 2537 | + "sucrase": "^3.35.0" | ||
| 2538 | + }, | ||
| 2539 | + "bin": { | ||
| 2540 | + "tailwind": "lib/cli.js", | ||
| 2541 | + "tailwindcss": "lib/cli.js" | ||
| 2542 | + }, | ||
| 2543 | + "engines": { | ||
| 2544 | + "node": ">=14.0.0" | ||
| 2545 | + } | ||
| 2546 | + }, | ||
| 2547 | + "node_modules/thenify": { | ||
| 2548 | + "version": "3.3.1", | ||
| 2549 | + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", | ||
| 2550 | + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", | ||
| 2551 | + "dev": true, | ||
| 2552 | + "license": "MIT", | ||
| 2553 | + "dependencies": { | ||
| 2554 | + "any-promise": "^1.0.0" | ||
| 2555 | + } | ||
| 2556 | + }, | ||
| 2557 | + "node_modules/thenify-all": { | ||
| 2558 | + "version": "1.6.0", | ||
| 2559 | + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", | ||
| 2560 | + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", | ||
| 2561 | + "dev": true, | ||
| 2562 | + "license": "MIT", | ||
| 2563 | + "dependencies": { | ||
| 2564 | + "thenify": ">= 3.1.0 < 4" | ||
| 2565 | + }, | ||
| 2566 | + "engines": { | ||
| 2567 | + "node": ">=0.8" | ||
| 2568 | + } | ||
| 2569 | + }, | ||
| 2570 | + "node_modules/tinyglobby": { | ||
| 2571 | + "version": "0.2.16", | ||
| 2572 | + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", | ||
| 2573 | + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", | ||
| 2574 | + "dev": true, | ||
| 2575 | + "license": "MIT", | ||
| 2576 | + "dependencies": { | ||
| 2577 | + "fdir": "^6.5.0", | ||
| 2578 | + "picomatch": "^4.0.4" | ||
| 2579 | + }, | ||
| 2580 | + "engines": { | ||
| 2581 | + "node": ">=12.0.0" | ||
| 2582 | + }, | ||
| 2583 | + "funding": { | ||
| 2584 | + "url": "https://github.com/sponsors/SuperchupuDev" | ||
| 2585 | + } | ||
| 2586 | + }, | ||
| 2587 | + "node_modules/tinyglobby/node_modules/fdir": { | ||
| 2588 | + "version": "6.5.0", | ||
| 2589 | + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", | ||
| 2590 | + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", | ||
| 2591 | + "dev": true, | ||
| 2592 | + "license": "MIT", | ||
| 2593 | + "engines": { | ||
| 2594 | + "node": ">=12.0.0" | ||
| 2595 | + }, | ||
| 2596 | + "peerDependencies": { | ||
| 2597 | + "picomatch": "^3 || ^4" | ||
| 2598 | + }, | ||
| 2599 | + "peerDependenciesMeta": { | ||
| 2600 | + "picomatch": { | ||
| 2601 | + "optional": true | ||
| 2602 | + } | ||
| 2603 | + } | ||
| 2604 | + }, | ||
| 2605 | + "node_modules/tinyglobby/node_modules/picomatch": { | ||
| 2606 | + "version": "4.0.4", | ||
| 2607 | + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", | ||
| 2608 | + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", | ||
| 2609 | + "dev": true, | ||
| 2610 | + "license": "MIT", | ||
| 2611 | + "engines": { | ||
| 2612 | + "node": ">=12" | ||
| 2613 | + }, | ||
| 2614 | + "funding": { | ||
| 2615 | + "url": "https://github.com/sponsors/jonschlinkert" | ||
| 2616 | + } | ||
| 2617 | + }, | ||
| 2618 | + "node_modules/to-regex-range": { | ||
| 2619 | + "version": "5.0.1", | ||
| 2620 | + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | ||
| 2621 | + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", | ||
| 2622 | + "dev": true, | ||
| 2623 | + "license": "MIT", | ||
| 2624 | + "dependencies": { | ||
| 2625 | + "is-number": "^7.0.0" | ||
| 2626 | + }, | ||
| 2627 | + "engines": { | ||
| 2628 | + "node": ">=8.0" | ||
| 2629 | + } | ||
| 2630 | + }, | ||
| 2631 | + "node_modules/ts-interface-checker": { | ||
| 2632 | + "version": "0.1.13", | ||
| 2633 | + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", | ||
| 2634 | + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", | ||
| 2635 | + "dev": true, | ||
| 2636 | + "license": "Apache-2.0" | ||
| 2637 | + }, | ||
| 2638 | + "node_modules/typescript": { | ||
| 2639 | + "version": "5.9.3", | ||
| 2640 | + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", | ||
| 2641 | + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", | ||
| 2642 | + "dev": true, | ||
| 2643 | + "license": "Apache-2.0", | ||
| 2644 | + "bin": { | ||
| 2645 | + "tsc": "bin/tsc", | ||
| 2646 | + "tsserver": "bin/tsserver" | ||
| 2647 | + }, | ||
| 2648 | + "engines": { | ||
| 2649 | + "node": ">=14.17" | ||
| 2650 | + } | ||
| 2651 | + }, | ||
| 2652 | + "node_modules/undici-types": { | ||
| 2653 | + "version": "7.18.2", | ||
| 2654 | + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", | ||
| 2655 | + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", | ||
| 2656 | + "dev": true, | ||
| 2657 | + "license": "MIT" | ||
| 2658 | + }, | ||
| 2659 | + "node_modules/update-browserslist-db": { | ||
| 2660 | + "version": "1.2.3", | ||
| 2661 | + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", | ||
| 2662 | + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", | ||
| 2663 | + "dev": true, | ||
| 2664 | + "funding": [ | ||
| 2665 | + { | ||
| 2666 | + "type": "opencollective", | ||
| 2667 | + "url": "https://opencollective.com/browserslist" | ||
| 2668 | + }, | ||
| 2669 | + { | ||
| 2670 | + "type": "tidelift", | ||
| 2671 | + "url": "https://tidelift.com/funding/github/npm/browserslist" | ||
| 2672 | + }, | ||
| 2673 | + { | ||
| 2674 | + "type": "github", | ||
| 2675 | + "url": "https://github.com/sponsors/ai" | ||
| 2676 | + } | ||
| 2677 | + ], | ||
| 2678 | + "license": "MIT", | ||
| 2679 | + "dependencies": { | ||
| 2680 | + "escalade": "^3.2.0", | ||
| 2681 | + "picocolors": "^1.1.1" | ||
| 2682 | + }, | ||
| 2683 | + "bin": { | ||
| 2684 | + "update-browserslist-db": "cli.js" | ||
| 2685 | + }, | ||
| 2686 | + "peerDependencies": { | ||
| 2687 | + "browserslist": ">= 4.21.0" | ||
| 2688 | + } | ||
| 2689 | + }, | ||
| 2690 | + "node_modules/util-deprecate": { | ||
| 2691 | + "version": "1.0.2", | ||
| 2692 | + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", | ||
| 2693 | + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", | ||
| 2694 | + "dev": true, | ||
| 2695 | + "license": "MIT" | ||
| 2696 | + }, | ||
| 2697 | + "node_modules/vite": { | ||
| 2698 | + "version": "5.4.21", | ||
| 2699 | + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", | ||
| 2700 | + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", | ||
| 2701 | + "dev": true, | ||
| 2702 | + "license": "MIT", | ||
| 2703 | + "dependencies": { | ||
| 2704 | + "esbuild": "^0.21.3", | ||
| 2705 | + "postcss": "^8.4.43", | ||
| 2706 | + "rollup": "^4.20.0" | ||
| 2707 | + }, | ||
| 2708 | + "bin": { | ||
| 2709 | + "vite": "bin/vite.js" | ||
| 2710 | + }, | ||
| 2711 | + "engines": { | ||
| 2712 | + "node": "^18.0.0 || >=20.0.0" | ||
| 2713 | + }, | ||
| 2714 | + "funding": { | ||
| 2715 | + "url": "https://github.com/vitejs/vite?sponsor=1" | ||
| 2716 | + }, | ||
| 2717 | + "optionalDependencies": { | ||
| 2718 | + "fsevents": "~2.3.3" | ||
| 2719 | + }, | ||
| 2720 | + "peerDependencies": { | ||
| 2721 | + "@types/node": "^18.0.0 || >=20.0.0", | ||
| 2722 | + "less": "*", | ||
| 2723 | + "lightningcss": "^1.21.0", | ||
| 2724 | + "sass": "*", | ||
| 2725 | + "sass-embedded": "*", | ||
| 2726 | + "stylus": "*", | ||
| 2727 | + "sugarss": "*", | ||
| 2728 | + "terser": "^5.4.0" | ||
| 2729 | + }, | ||
| 2730 | + "peerDependenciesMeta": { | ||
| 2731 | + "@types/node": { | ||
| 2732 | + "optional": true | ||
| 2733 | + }, | ||
| 2734 | + "less": { | ||
| 2735 | + "optional": true | ||
| 2736 | + }, | ||
| 2737 | + "lightningcss": { | ||
| 2738 | + "optional": true | ||
| 2739 | + }, | ||
| 2740 | + "sass": { | ||
| 2741 | + "optional": true | ||
| 2742 | + }, | ||
| 2743 | + "sass-embedded": { | ||
| 2744 | + "optional": true | ||
| 2745 | + }, | ||
| 2746 | + "stylus": { | ||
| 2747 | + "optional": true | ||
| 2748 | + }, | ||
| 2749 | + "sugarss": { | ||
| 2750 | + "optional": true | ||
| 2751 | + }, | ||
| 2752 | + "terser": { | ||
| 2753 | + "optional": true | ||
| 2754 | + } | ||
| 2755 | + } | ||
| 2756 | + }, | ||
| 2757 | + "node_modules/yallist": { | ||
| 2758 | + "version": "3.1.1", | ||
| 2759 | + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", | ||
| 2760 | + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", | ||
| 2761 | + "dev": true, | ||
| 2762 | + "license": "ISC" | ||
| 2763 | + } | ||
| 2764 | + } | ||
| 2765 | +} |
web/package.json
0 → 100644
| 1 | +{ | ||
| 2 | + "name": "vibe-erp-web", | ||
| 3 | + "private": true, | ||
| 4 | + "version": "0.0.0", | ||
| 5 | + "type": "module", | ||
| 6 | + "scripts": { | ||
| 7 | + "dev": "vite", | ||
| 8 | + "build": "tsc -b && vite build", | ||
| 9 | + "preview": "vite preview" | ||
| 10 | + }, | ||
| 11 | + "dependencies": { | ||
| 12 | + "react": "^18.3.1", | ||
| 13 | + "react-dom": "^18.3.1", | ||
| 14 | + "react-router-dom": "^6.28.0" | ||
| 15 | + }, | ||
| 16 | + "devDependencies": { | ||
| 17 | + "@types/node": "^25.5.2", | ||
| 18 | + "@types/react": "^18.3.12", | ||
| 19 | + "@types/react-dom": "^18.3.1", | ||
| 20 | + "@vitejs/plugin-react": "^4.3.3", | ||
| 21 | + "autoprefixer": "^10.4.20", | ||
| 22 | + "postcss": "^8.4.49", | ||
| 23 | + "tailwindcss": "^3.4.15", | ||
| 24 | + "typescript": "^5.6.3", | ||
| 25 | + "vite": "^5.4.11" | ||
| 26 | + } | ||
| 27 | +} |
web/postcss.config.js
0 → 100644
web/src/App.tsx
0 → 100644
| 1 | +// vibe_erp web SPA route table. | ||
| 2 | +// | ||
| 3 | +// One <Routes> for the whole SPA: /login is open, everything else | ||
| 4 | +// nests under <ProtectedRoute><AppLayout/>. The layout component | ||
| 5 | +// renders <Outlet/> for the active child route. | ||
| 6 | +// | ||
| 7 | +// **Why a flat route table.** v1 has 12 screens; nested routing | ||
| 8 | +// would just be ceremony. Future chunks can introduce nested | ||
| 9 | +// routes when, e.g., the Settings area gets sub-tabs. | ||
| 10 | + | ||
| 11 | +import { Navigate, Route, Routes } from 'react-router-dom' | ||
| 12 | +import { AppLayout } from '@/layout/AppLayout' | ||
| 13 | +import { ProtectedRoute } from '@/components/ProtectedRoute' | ||
| 14 | +import { LoginPage } from '@/pages/LoginPage' | ||
| 15 | +import { DashboardPage } from '@/pages/DashboardPage' | ||
| 16 | +import { ItemsPage } from '@/pages/ItemsPage' | ||
| 17 | +import { UomsPage } from '@/pages/UomsPage' | ||
| 18 | +import { PartnersPage } from '@/pages/PartnersPage' | ||
| 19 | +import { LocationsPage } from '@/pages/LocationsPage' | ||
| 20 | +import { BalancesPage } from '@/pages/BalancesPage' | ||
| 21 | +import { MovementsPage } from '@/pages/MovementsPage' | ||
| 22 | +import { SalesOrdersPage } from '@/pages/SalesOrdersPage' | ||
| 23 | +import { SalesOrderDetailPage } from '@/pages/SalesOrderDetailPage' | ||
| 24 | +import { PurchaseOrdersPage } from '@/pages/PurchaseOrdersPage' | ||
| 25 | +import { PurchaseOrderDetailPage } from '@/pages/PurchaseOrderDetailPage' | ||
| 26 | +import { WorkOrdersPage } from '@/pages/WorkOrdersPage' | ||
| 27 | +import { WorkOrderDetailPage } from '@/pages/WorkOrderDetailPage' | ||
| 28 | +import { ShopFloorPage } from '@/pages/ShopFloorPage' | ||
| 29 | +import { JournalEntriesPage } from '@/pages/JournalEntriesPage' | ||
| 30 | + | ||
| 31 | +export default function App() { | ||
| 32 | + return ( | ||
| 33 | + <Routes> | ||
| 34 | + <Route path="/login" element={<LoginPage />} /> | ||
| 35 | + <Route | ||
| 36 | + path="/" | ||
| 37 | + element={ | ||
| 38 | + <ProtectedRoute> | ||
| 39 | + <AppLayout /> | ||
| 40 | + </ProtectedRoute> | ||
| 41 | + } | ||
| 42 | + > | ||
| 43 | + <Route index element={<DashboardPage />} /> | ||
| 44 | + <Route path="items" element={<ItemsPage />} /> | ||
| 45 | + <Route path="uoms" element={<UomsPage />} /> | ||
| 46 | + <Route path="partners" element={<PartnersPage />} /> | ||
| 47 | + <Route path="locations" element={<LocationsPage />} /> | ||
| 48 | + <Route path="balances" element={<BalancesPage />} /> | ||
| 49 | + <Route path="movements" element={<MovementsPage />} /> | ||
| 50 | + <Route path="sales-orders" element={<SalesOrdersPage />} /> | ||
| 51 | + <Route path="sales-orders/:id" element={<SalesOrderDetailPage />} /> | ||
| 52 | + <Route path="purchase-orders" element={<PurchaseOrdersPage />} /> | ||
| 53 | + <Route path="purchase-orders/:id" element={<PurchaseOrderDetailPage />} /> | ||
| 54 | + <Route path="work-orders" element={<WorkOrdersPage />} /> | ||
| 55 | + <Route path="work-orders/:id" element={<WorkOrderDetailPage />} /> | ||
| 56 | + <Route path="shop-floor" element={<ShopFloorPage />} /> | ||
| 57 | + <Route path="journal-entries" element={<JournalEntriesPage />} /> | ||
| 58 | + </Route> | ||
| 59 | + <Route path="*" element={<Navigate to="/" replace />} /> | ||
| 60 | + </Routes> | ||
| 61 | + ) | ||
| 62 | +} |
web/src/api/client.ts
0 → 100644
| 1 | +// vibe_erp typed REST client. | ||
| 2 | +// | ||
| 3 | +// **Why a thin wrapper instead of a generated client.** The framework | ||
| 4 | +// already serves a real OpenAPI spec at /v3/api-docs and could feed | ||
| 5 | +// openapi-typescript or openapi-generator-cli — but every code | ||
| 6 | +// generator pulls a chain of npm/Java toolchain steps into the build | ||
| 7 | +// and generates ~thousands of lines of churn for every backend tweak. | ||
| 8 | +// A hand-written client over `fetch` keeps the SPA bundle small, | ||
| 9 | +// auditable, and dependency-free, and v1 of the SPA only needs the | ||
| 10 | +// few dozen calls listed below. Codegen can return when (a) the | ||
| 11 | +// API surface is too large to maintain by hand or (b) external | ||
| 12 | +// integrators want a typed client they can reuse — neither is true | ||
| 13 | +// in v1.0. | ||
| 14 | +// | ||
| 15 | +// **Auth.** Every call goes through `apiFetch`, which reads the | ||
| 16 | +// access token from localStorage and adds an `Authorization: Bearer | ||
| 17 | +// <token>` header. A 401 response triggers a hard logout (clears | ||
| 18 | +// the token + redirects to /login via the auth context) — not a | ||
| 19 | +// silent refresh, because v1 keeps the refresh story simple. | ||
| 20 | +// Refreshing on 401 is a v1.x improvement. | ||
| 21 | +// | ||
| 22 | +// **Errors.** A non-2xx response throws an `ApiError` carrying the | ||
| 23 | +// status, message, and the parsed JSON body if any. Pages catch | ||
| 24 | +// this and render the message inline. Network failures throw the | ||
| 25 | +// underlying TypeError, also caught by the page boundary. | ||
| 26 | + | ||
| 27 | +import type { | ||
| 28 | + Item, | ||
| 29 | + JournalEntry, | ||
| 30 | + Location, | ||
| 31 | + MetaInfo, | ||
| 32 | + Partner, | ||
| 33 | + PurchaseOrder, | ||
| 34 | + SalesOrder, | ||
| 35 | + ShopFloorEntry, | ||
| 36 | + StockBalance, | ||
| 37 | + StockMovement, | ||
| 38 | + TokenPair, | ||
| 39 | + Uom, | ||
| 40 | + WorkOrder, | ||
| 41 | +} from '@/types/api' | ||
| 42 | + | ||
| 43 | +const TOKEN_KEY = 'vibeerp.accessToken' | ||
| 44 | + | ||
| 45 | +export function getAccessToken(): string | null { | ||
| 46 | + return localStorage.getItem(TOKEN_KEY) | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +export function setAccessToken(token: string | null): void { | ||
| 50 | + if (token === null) { | ||
| 51 | + localStorage.removeItem(TOKEN_KEY) | ||
| 52 | + } else { | ||
| 53 | + localStorage.setItem(TOKEN_KEY, token) | ||
| 54 | + } | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +export class ApiError extends Error { | ||
| 58 | + constructor( | ||
| 59 | + message: string, | ||
| 60 | + public readonly status: number, | ||
| 61 | + public readonly body: unknown, | ||
| 62 | + ) { | ||
| 63 | + super(message) | ||
| 64 | + this.name = 'ApiError' | ||
| 65 | + } | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +// One-time-set callback that the auth context registers so the | ||
| 69 | +// client can trigger a logout on 401 without a circular import. | ||
| 70 | +let onUnauthorized: (() => void) | null = null | ||
| 71 | +export function registerUnauthorizedHandler(handler: () => void) { | ||
| 72 | + onUnauthorized = handler | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +async function apiFetch<T>( | ||
| 76 | + path: string, | ||
| 77 | + init: RequestInit = {}, | ||
| 78 | + expectJson = true, | ||
| 79 | +): Promise<T> { | ||
| 80 | + const headers = new Headers(init.headers) | ||
| 81 | + headers.set('Accept', 'application/json') | ||
| 82 | + if (init.body && !headers.has('Content-Type')) { | ||
| 83 | + headers.set('Content-Type', 'application/json') | ||
| 84 | + } | ||
| 85 | + const token = getAccessToken() | ||
| 86 | + if (token) headers.set('Authorization', `Bearer ${token}`) | ||
| 87 | + | ||
| 88 | + const res = await fetch(path, { ...init, headers }) | ||
| 89 | + | ||
| 90 | + if (res.status === 401) { | ||
| 91 | + if (onUnauthorized) onUnauthorized() | ||
| 92 | + throw new ApiError('Not authenticated', 401, null) | ||
| 93 | + } | ||
| 94 | + if (!res.ok) { | ||
| 95 | + let body: unknown = null | ||
| 96 | + let message = `${res.status} ${res.statusText}` | ||
| 97 | + try { | ||
| 98 | + const text = await res.text() | ||
| 99 | + if (text) { | ||
| 100 | + try { | ||
| 101 | + body = JSON.parse(text) | ||
| 102 | + const m = (body as { message?: unknown }).message | ||
| 103 | + if (typeof m === 'string') message = m | ||
| 104 | + } catch { | ||
| 105 | + body = text | ||
| 106 | + message = text | ||
| 107 | + } | ||
| 108 | + } | ||
| 109 | + } catch { | ||
| 110 | + // ignore body parse errors | ||
| 111 | + } | ||
| 112 | + throw new ApiError(message, res.status, body) | ||
| 113 | + } | ||
| 114 | + if (!expectJson || res.status === 204) { | ||
| 115 | + return undefined as T | ||
| 116 | + } | ||
| 117 | + return (await res.json()) as T | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +// ─── Public unauthenticated calls ──────────────────────────────────── | ||
| 121 | + | ||
| 122 | +export const meta = { | ||
| 123 | + info: () => apiFetch<MetaInfo>('/api/v1/_meta/info'), | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +export const auth = { | ||
| 127 | + login: (username: string, password: string) => | ||
| 128 | + apiFetch<TokenPair>('/api/v1/auth/login', { | ||
| 129 | + method: 'POST', | ||
| 130 | + body: JSON.stringify({ username, password }), | ||
| 131 | + }), | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +// ─── Catalog ───────────────────────────────────────────────────────── | ||
| 135 | + | ||
| 136 | +export const catalog = { | ||
| 137 | + listItems: () => apiFetch<Item[]>('/api/v1/catalog/items'), | ||
| 138 | + getItem: (id: string) => apiFetch<Item>(`/api/v1/catalog/items/${id}`), | ||
| 139 | + listUoms: () => apiFetch<Uom[]>('/api/v1/catalog/uoms'), | ||
| 140 | +} | ||
| 141 | + | ||
| 142 | +// ─── Partners ──────────────────────────────────────────────────────── | ||
| 143 | + | ||
| 144 | +export const partners = { | ||
| 145 | + list: () => apiFetch<Partner[]>('/api/v1/partners/partners'), | ||
| 146 | + get: (id: string) => apiFetch<Partner>(`/api/v1/partners/partners/${id}`), | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +// ─── Inventory ─────────────────────────────────────────────────────── | ||
| 150 | + | ||
| 151 | +export const inventory = { | ||
| 152 | + listLocations: () => apiFetch<Location[]>('/api/v1/inventory/locations'), | ||
| 153 | + listBalances: () => apiFetch<StockBalance[]>('/api/v1/inventory/balances'), | ||
| 154 | + listMovements: () => apiFetch<StockMovement[]>('/api/v1/inventory/movements'), | ||
| 155 | +} | ||
| 156 | + | ||
| 157 | +// ─── Sales orders ──────────────────────────────────────────────────── | ||
| 158 | + | ||
| 159 | +export const salesOrders = { | ||
| 160 | + list: () => apiFetch<SalesOrder[]>('/api/v1/orders/sales-orders'), | ||
| 161 | + get: (id: string) => apiFetch<SalesOrder>(`/api/v1/orders/sales-orders/${id}`), | ||
| 162 | + confirm: (id: string) => | ||
| 163 | + apiFetch<SalesOrder>(`/api/v1/orders/sales-orders/${id}/confirm`, { | ||
| 164 | + method: 'POST', | ||
| 165 | + }), | ||
| 166 | + cancel: (id: string) => | ||
| 167 | + apiFetch<SalesOrder>(`/api/v1/orders/sales-orders/${id}/cancel`, { | ||
| 168 | + method: 'POST', | ||
| 169 | + }), | ||
| 170 | + ship: (id: string, shippingLocationCode: string) => | ||
| 171 | + apiFetch<SalesOrder>(`/api/v1/orders/sales-orders/${id}/ship`, { | ||
| 172 | + method: 'POST', | ||
| 173 | + body: JSON.stringify({ shippingLocationCode }), | ||
| 174 | + }), | ||
| 175 | +} | ||
| 176 | + | ||
| 177 | +// ─── Purchase orders ───────────────────────────────────────────────── | ||
| 178 | + | ||
| 179 | +export const purchaseOrders = { | ||
| 180 | + list: () => apiFetch<PurchaseOrder[]>('/api/v1/orders/purchase-orders'), | ||
| 181 | + get: (id: string) => apiFetch<PurchaseOrder>(`/api/v1/orders/purchase-orders/${id}`), | ||
| 182 | + confirm: (id: string) => | ||
| 183 | + apiFetch<PurchaseOrder>(`/api/v1/orders/purchase-orders/${id}/confirm`, { | ||
| 184 | + method: 'POST', | ||
| 185 | + }), | ||
| 186 | + cancel: (id: string) => | ||
| 187 | + apiFetch<PurchaseOrder>(`/api/v1/orders/purchase-orders/${id}/cancel`, { | ||
| 188 | + method: 'POST', | ||
| 189 | + }), | ||
| 190 | + receive: (id: string, receivingLocationCode: string) => | ||
| 191 | + apiFetch<PurchaseOrder>(`/api/v1/orders/purchase-orders/${id}/receive`, { | ||
| 192 | + method: 'POST', | ||
| 193 | + body: JSON.stringify({ receivingLocationCode }), | ||
| 194 | + }), | ||
| 195 | +} | ||
| 196 | + | ||
| 197 | +// ─── Production ────────────────────────────────────────────────────── | ||
| 198 | + | ||
| 199 | +export const production = { | ||
| 200 | + listWorkOrders: () => apiFetch<WorkOrder[]>('/api/v1/production/work-orders'), | ||
| 201 | + getWorkOrder: (id: string) => | ||
| 202 | + apiFetch<WorkOrder>(`/api/v1/production/work-orders/${id}`), | ||
| 203 | + startWorkOrder: (id: string) => | ||
| 204 | + apiFetch<WorkOrder>(`/api/v1/production/work-orders/${id}/start`, { | ||
| 205 | + method: 'POST', | ||
| 206 | + }), | ||
| 207 | + completeWorkOrder: (id: string, outputLocationCode: string) => | ||
| 208 | + apiFetch<WorkOrder>(`/api/v1/production/work-orders/${id}/complete`, { | ||
| 209 | + method: 'POST', | ||
| 210 | + body: JSON.stringify({ outputLocationCode }), | ||
| 211 | + }), | ||
| 212 | + cancelWorkOrder: (id: string) => | ||
| 213 | + apiFetch<WorkOrder>(`/api/v1/production/work-orders/${id}/cancel`, { | ||
| 214 | + method: 'POST', | ||
| 215 | + }), | ||
| 216 | + shopFloor: () => | ||
| 217 | + apiFetch<ShopFloorEntry[]>('/api/v1/production/work-orders/shop-floor'), | ||
| 218 | +} | ||
| 219 | + | ||
| 220 | +// ─── Finance ───────────────────────────────────────────────────────── | ||
| 221 | + | ||
| 222 | +export const finance = { | ||
| 223 | + listJournalEntries: () => | ||
| 224 | + apiFetch<JournalEntry[]>('/api/v1/finance/journal-entries'), | ||
| 225 | +} |
web/src/auth/AuthContext.tsx
0 → 100644
| 1 | +// vibe_erp web auth context. | ||
| 2 | +// | ||
| 3 | +// **Scope.** Holds the in-memory copy of the access token + the user | ||
| 4 | +// it was issued to (decoded from the JWT payload), exposes login / | ||
| 5 | +// logout actions, and rerenders subscribers when the token state | ||
| 6 | +// flips. Persistence lives in localStorage via the api client's | ||
| 7 | +// `getAccessToken/setAccessToken` helpers — the context only mirrors | ||
| 8 | +// what's in storage so a hard refresh of the page picks up the | ||
| 9 | +// existing session without a re-login. | ||
| 10 | +// | ||
| 11 | +// **Why decode the JWT in the SPA.** vibe_erp's auth tokens are | ||
| 12 | +// signed (HS256) by the framework, so the SPA cannot *trust* the | ||
| 13 | +// payload — but it can DISPLAY it (username, role list) without a | ||
| 14 | +// round-trip. Anything load-bearing (permission checks) still | ||
| 15 | +// happens server-side. The decode is intentionally trivial: split | ||
| 16 | +// on '.', base64-decode the middle segment, JSON.parse. No JWT lib | ||
| 17 | +// dependency; an invalid token just degrades to "unknown user" and | ||
| 18 | +// the next API call gets a 401, triggering a logout. | ||
| 19 | +// | ||
| 20 | +// **401 handling.** The api client calls registerUnauthorizedHandler | ||
| 21 | +// once at boot. When any request returns 401, the handler clears | ||
| 22 | +// the token and forces a navigate to /login (preserving the | ||
| 23 | +// attempted path so post-login can redirect back). | ||
| 24 | + | ||
| 25 | +import { | ||
| 26 | + createContext, | ||
| 27 | + useCallback, | ||
| 28 | + useContext, | ||
| 29 | + useEffect, | ||
| 30 | + useMemo, | ||
| 31 | + useState, | ||
| 32 | + type ReactNode, | ||
| 33 | +} from 'react' | ||
| 34 | +import { useLocation, useNavigate } from 'react-router-dom' | ||
| 35 | +import { | ||
| 36 | + auth as authApi, | ||
| 37 | + getAccessToken, | ||
| 38 | + registerUnauthorizedHandler, | ||
| 39 | + setAccessToken, | ||
| 40 | +} from '@/api/client' | ||
| 41 | + | ||
| 42 | +interface JwtPayload { | ||
| 43 | + sub?: string | ||
| 44 | + username?: string | ||
| 45 | + roles?: string[] | ||
| 46 | + exp?: number | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +function decodeJwt(token: string): JwtPayload | null { | ||
| 50 | + try { | ||
| 51 | + const parts = token.split('.') | ||
| 52 | + if (parts.length !== 3) return null | ||
| 53 | + const padded = parts[1] + '==='.slice((parts[1].length + 3) % 4) | ||
| 54 | + const json = atob(padded.replace(/-/g, '+').replace(/_/g, '/')) | ||
| 55 | + return JSON.parse(json) as JwtPayload | ||
| 56 | + } catch { | ||
| 57 | + return null | ||
| 58 | + } | ||
| 59 | +} | ||
| 60 | + | ||
| 61 | +interface AuthState { | ||
| 62 | + token: string | null | ||
| 63 | + username: string | null | ||
| 64 | + roles: string[] | ||
| 65 | + loading: boolean | ||
| 66 | + login: (username: string, password: string) => Promise<void> | ||
| 67 | + logout: () => void | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +const AuthContext = createContext<AuthState | null>(null) | ||
| 71 | + | ||
| 72 | +export function AuthProvider({ children }: { children: ReactNode }) { | ||
| 73 | + const navigate = useNavigate() | ||
| 74 | + const location = useLocation() | ||
| 75 | + const [token, setToken] = useState<string | null>(() => getAccessToken()) | ||
| 76 | + const [loading, setLoading] = useState(false) | ||
| 77 | + | ||
| 78 | + const payload = useMemo(() => (token ? decodeJwt(token) : null), [token]) | ||
| 79 | + | ||
| 80 | + const logout = useCallback(() => { | ||
| 81 | + setAccessToken(null) | ||
| 82 | + setToken(null) | ||
| 83 | + if (location.pathname !== '/login') { | ||
| 84 | + navigate('/login', { | ||
| 85 | + replace: true, | ||
| 86 | + state: { from: location.pathname + location.search }, | ||
| 87 | + }) | ||
| 88 | + } | ||
| 89 | + }, [navigate, location]) | ||
| 90 | + | ||
| 91 | + // Wire the api client's 401 handler exactly once. | ||
| 92 | + useEffect(() => { | ||
| 93 | + registerUnauthorizedHandler(() => { | ||
| 94 | + // Defer to next tick so the throw inside apiFetch isn't | ||
| 95 | + // swallowed by React's render-phase complaints. | ||
| 96 | + setTimeout(() => logout(), 0) | ||
| 97 | + }) | ||
| 98 | + }, [logout]) | ||
| 99 | + | ||
| 100 | + const login = useCallback( | ||
| 101 | + async (username: string, password: string) => { | ||
| 102 | + setLoading(true) | ||
| 103 | + try { | ||
| 104 | + const pair = await authApi.login(username, password) | ||
| 105 | + setAccessToken(pair.accessToken) | ||
| 106 | + setToken(pair.accessToken) | ||
| 107 | + } finally { | ||
| 108 | + setLoading(false) | ||
| 109 | + } | ||
| 110 | + }, | ||
| 111 | + [], | ||
| 112 | + ) | ||
| 113 | + | ||
| 114 | + const value: AuthState = { | ||
| 115 | + token, | ||
| 116 | + username: payload?.username ?? payload?.sub ?? null, | ||
| 117 | + roles: payload?.roles ?? [], | ||
| 118 | + loading, | ||
| 119 | + login, | ||
| 120 | + logout, | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + return <AuthContext.Provider value={value}>{children}</AuthContext.Provider> | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +export function useAuth(): AuthState { | ||
| 127 | + const ctx = useContext(AuthContext) | ||
| 128 | + if (!ctx) throw new Error('useAuth must be used inside <AuthProvider>') | ||
| 129 | + return ctx | ||
| 130 | +} |
web/src/components/DataTable.tsx
0 → 100644
| 1 | +// Tiny generic table component. | ||
| 2 | +// | ||
| 3 | +// Each column declares: a header label, a key, and an optional | ||
| 4 | +// `render` function that takes the row and returns a ReactNode. | ||
| 5 | +// Without `render` the column reads `row[key]` and stringifies it. | ||
| 6 | +// | ||
| 7 | +// Intentionally not a heavy table lib (TanStack Table, AG Grid). | ||
| 8 | +// v1 SPA needs columnar reads, link cells, status badges, and | ||
| 9 | +// filtering by a free-text input — all trivially handcoded. A | ||
| 10 | +// future chunk can swap this out the day a real consumer asks for | ||
| 11 | +// virtualised rows or column resizing. | ||
| 12 | + | ||
| 13 | +import type { ReactNode } from 'react' | ||
| 14 | + | ||
| 15 | +export interface Column<T> { | ||
| 16 | + header: string | ||
| 17 | + key: string | ||
| 18 | + render?: (row: T) => ReactNode | ||
| 19 | + className?: string | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +interface Props<T> { | ||
| 23 | + rows: T[] | ||
| 24 | + columns: Column<T>[] | ||
| 25 | + empty?: ReactNode | ||
| 26 | + rowKey?: (row: T) => string | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +export function DataTable<T>({ | ||
| 30 | + rows, | ||
| 31 | + columns, | ||
| 32 | + empty = <div className="p-6 text-sm text-slate-400">No rows.</div>, | ||
| 33 | + rowKey, | ||
| 34 | +}: Props<T>) { | ||
| 35 | + if (rows.length === 0) { | ||
| 36 | + return <div className="card">{empty}</div> | ||
| 37 | + } | ||
| 38 | + return ( | ||
| 39 | + <div className="card overflow-x-auto"> | ||
| 40 | + <table className="table-base"> | ||
| 41 | + <thead className="bg-slate-50"> | ||
| 42 | + <tr> | ||
| 43 | + {columns.map((c) => ( | ||
| 44 | + <th key={c.key} className={c.className}> | ||
| 45 | + {c.header} | ||
| 46 | + </th> | ||
| 47 | + ))} | ||
| 48 | + </tr> | ||
| 49 | + </thead> | ||
| 50 | + <tbody className="divide-y divide-slate-100"> | ||
| 51 | + {rows.map((row, idx) => { | ||
| 52 | + const key = rowKey | ||
| 53 | + ? rowKey(row) | ||
| 54 | + : ((row as { id?: string }).id ?? String(idx)) | ||
| 55 | + return ( | ||
| 56 | + <tr key={key} className="hover:bg-slate-50"> | ||
| 57 | + {columns.map((c) => ( | ||
| 58 | + <td key={c.key} className={c.className}> | ||
| 59 | + {c.render | ||
| 60 | + ? c.render(row) | ||
| 61 | + : ((row as unknown as Record<string, unknown>)[c.key] as ReactNode) ?? ''} | ||
| 62 | + </td> | ||
| 63 | + ))} | ||
| 64 | + </tr> | ||
| 65 | + ) | ||
| 66 | + })} | ||
| 67 | + </tbody> | ||
| 68 | + </table> | ||
| 69 | + </div> | ||
| 70 | + ) | ||
| 71 | +} |
web/src/components/ErrorBox.tsx
0 → 100644
| 1 | +interface Props { | ||
| 2 | + error: unknown | ||
| 3 | +} | ||
| 4 | + | ||
| 5 | +export function ErrorBox({ error }: Props) { | ||
| 6 | + let message: string | ||
| 7 | + if (error instanceof Error) { | ||
| 8 | + message = error.message | ||
| 9 | + } else if (typeof error === 'string') { | ||
| 10 | + message = error | ||
| 11 | + } else if (error && typeof error === 'object' && 'message' in error) { | ||
| 12 | + message = String((error as { message: unknown }).message) | ||
| 13 | + } else { | ||
| 14 | + message = 'Unknown error' | ||
| 15 | + } | ||
| 16 | + return ( | ||
| 17 | + <div className="rounded-md border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800"> | ||
| 18 | + <span className="font-semibold">Error:</span> {message} | ||
| 19 | + </div> | ||
| 20 | + ) | ||
| 21 | +} |
web/src/components/Loading.tsx
0 → 100644
web/src/components/PageHeader.tsx
0 → 100644
| 1 | +import type { ReactNode } from 'react' | ||
| 2 | + | ||
| 3 | +interface Props { | ||
| 4 | + title: string | ||
| 5 | + subtitle?: string | ||
| 6 | + actions?: ReactNode | ||
| 7 | +} | ||
| 8 | + | ||
| 9 | +export function PageHeader({ title, subtitle, actions }: Props) { | ||
| 10 | + return ( | ||
| 11 | + <div className="mb-6 flex flex-col items-start gap-3 sm:flex-row sm:items-end sm:justify-between"> | ||
| 12 | + <div> | ||
| 13 | + <h1 className="text-2xl font-semibold text-slate-900">{title}</h1> | ||
| 14 | + {subtitle && <p className="mt-1 text-sm text-slate-500">{subtitle}</p>} | ||
| 15 | + </div> | ||
| 16 | + {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>} | ||
| 17 | + </div> | ||
| 18 | + ) | ||
| 19 | +} |
web/src/components/ProtectedRoute.tsx
0 → 100644
| 1 | +import { Navigate, useLocation } from 'react-router-dom' | ||
| 2 | +import { useAuth } from '@/auth/AuthContext' | ||
| 3 | +import type { ReactNode } from 'react' | ||
| 4 | + | ||
| 5 | +export function ProtectedRoute({ children }: { children: ReactNode }) { | ||
| 6 | + const { token } = useAuth() | ||
| 7 | + const location = useLocation() | ||
| 8 | + if (!token) { | ||
| 9 | + return ( | ||
| 10 | + <Navigate | ||
| 11 | + to="/login" | ||
| 12 | + replace | ||
| 13 | + state={{ from: location.pathname + location.search }} | ||
| 14 | + /> | ||
| 15 | + ) | ||
| 16 | + } | ||
| 17 | + return <>{children}</> | ||
| 18 | +} |
web/src/components/StatusBadge.tsx
0 → 100644
| 1 | +interface Props { | ||
| 2 | + status: string | ||
| 3 | +} | ||
| 4 | + | ||
| 5 | +// Status → Tailwind class via the shared @layer components classes | ||
| 6 | +// declared in index.css. Falls back to 'badge-pending' for unknown | ||
| 7 | +// statuses so a new server-side enum value renders as a neutral pill | ||
| 8 | +// instead of throwing. | ||
| 9 | +export function StatusBadge({ status }: Props) { | ||
| 10 | + const cls = | ||
| 11 | + { | ||
| 12 | + DRAFT: 'badge-draft', | ||
| 13 | + CONFIRMED: 'badge-confirmed', | ||
| 14 | + SHIPPED: 'badge-shipped', | ||
| 15 | + RECEIVED: 'badge-received', | ||
| 16 | + COMPLETED: 'badge-completed', | ||
| 17 | + CANCELLED: 'badge-cancelled', | ||
| 18 | + IN_PROGRESS: 'badge-in-progress', | ||
| 19 | + PENDING: 'badge-pending', | ||
| 20 | + POSTED: 'badge-confirmed', | ||
| 21 | + SETTLED: 'badge-shipped', | ||
| 22 | + REVERSED: 'badge-cancelled', | ||
| 23 | + }[status] ?? 'badge-pending' | ||
| 24 | + return <span className={cls}>{status}</span> | ||
| 25 | +} |
web/src/index.css
0 → 100644
| 1 | +@tailwind base; | ||
| 2 | +@tailwind components; | ||
| 3 | +@tailwind utilities; | ||
| 4 | + | ||
| 5 | +:root { | ||
| 6 | + font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; | ||
| 7 | + -webkit-font-smoothing: antialiased; | ||
| 8 | + -moz-osx-font-smoothing: grayscale; | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +@layer components { | ||
| 12 | + .btn { | ||
| 13 | + @apply inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium shadow-sm transition focus:outline-none focus:ring-2 focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50; | ||
| 14 | + } | ||
| 15 | + .btn-primary { | ||
| 16 | + @apply btn bg-brand-500 text-white hover:bg-brand-600 focus:ring-brand-500; | ||
| 17 | + } | ||
| 18 | + .btn-secondary { | ||
| 19 | + @apply btn bg-white text-slate-800 ring-1 ring-inset ring-slate-300 hover:bg-slate-50 focus:ring-brand-500; | ||
| 20 | + } | ||
| 21 | + .btn-danger { | ||
| 22 | + @apply btn bg-rose-600 text-white hover:bg-rose-700 focus:ring-rose-600; | ||
| 23 | + } | ||
| 24 | + .card { | ||
| 25 | + @apply rounded-lg border border-slate-200 bg-white shadow-sm; | ||
| 26 | + } | ||
| 27 | + .table-base { | ||
| 28 | + @apply min-w-full divide-y divide-slate-200 text-sm; | ||
| 29 | + } | ||
| 30 | + .table-base th { | ||
| 31 | + @apply px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-slate-500; | ||
| 32 | + } | ||
| 33 | + .table-base td { | ||
| 34 | + @apply whitespace-nowrap px-3 py-2 text-slate-700; | ||
| 35 | + } | ||
| 36 | + .badge { | ||
| 37 | + @apply inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold; | ||
| 38 | + } | ||
| 39 | + .badge-draft { @apply badge bg-slate-100 text-slate-700; } | ||
| 40 | + .badge-confirmed { @apply badge bg-amber-100 text-amber-800; } | ||
| 41 | + .badge-shipped { @apply badge bg-emerald-100 text-emerald-800; } | ||
| 42 | + .badge-received { @apply badge bg-emerald-100 text-emerald-800; } | ||
| 43 | + .badge-completed { @apply badge bg-emerald-100 text-emerald-800; } | ||
| 44 | + .badge-cancelled { @apply badge bg-rose-100 text-rose-800; } | ||
| 45 | + .badge-in-progress { @apply badge bg-sky-100 text-sky-800; } | ||
| 46 | + .badge-pending { @apply badge bg-slate-100 text-slate-600; } | ||
| 47 | +} |
web/src/layout/AppLayout.tsx
0 → 100644
| 1 | +// vibe_erp main app shell. | ||
| 2 | +// | ||
| 3 | +// Top bar (logo + version + user + logout) and a permanent left | ||
| 4 | +// sidebar grouped by Packaged Business Capability (PBC). The | ||
| 5 | +// `<Outlet />` renders whichever child route the router matched. | ||
| 6 | +// | ||
| 7 | +// **Why a static nav array.** The framework already exposes | ||
| 8 | +// menu metadata at /api/v1/_meta/metadata, but plumbing that into | ||
| 9 | +// the SPA is a Phase 3 (Tier 1 customization) concern. v1 SPA | ||
| 10 | +// hard-codes a sensible default that mirrors the implemented PBCs; | ||
| 11 | +// a future chunk replaces this with a metadata-driven sidebar so | ||
| 12 | +// plug-ins can contribute new menus through their YAML. | ||
| 13 | + | ||
| 14 | +import { NavLink, Outlet } from 'react-router-dom' | ||
| 15 | +import { useEffect, useState } from 'react' | ||
| 16 | +import { useAuth } from '@/auth/AuthContext' | ||
| 17 | +import { meta } from '@/api/client' | ||
| 18 | +import type { MetaInfo } from '@/types/api' | ||
| 19 | + | ||
| 20 | +interface NavItem { | ||
| 21 | + to: string | ||
| 22 | + label: string | ||
| 23 | +} | ||
| 24 | +interface NavGroup { | ||
| 25 | + heading: string | ||
| 26 | + items: NavItem[] | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +const NAV: NavGroup[] = [ | ||
| 30 | + { | ||
| 31 | + heading: 'Overview', | ||
| 32 | + items: [{ to: '/', label: 'Dashboard' }], | ||
| 33 | + }, | ||
| 34 | + { | ||
| 35 | + heading: 'Catalog & Partners', | ||
| 36 | + items: [ | ||
| 37 | + { to: '/items', label: 'Items' }, | ||
| 38 | + { to: '/uoms', label: 'Units of Measure' }, | ||
| 39 | + { to: '/partners', label: 'Partners' }, | ||
| 40 | + ], | ||
| 41 | + }, | ||
| 42 | + { | ||
| 43 | + heading: 'Inventory', | ||
| 44 | + items: [ | ||
| 45 | + { to: '/locations', label: 'Locations' }, | ||
| 46 | + { to: '/balances', label: 'Stock Balances' }, | ||
| 47 | + { to: '/movements', label: 'Stock Movements' }, | ||
| 48 | + ], | ||
| 49 | + }, | ||
| 50 | + { | ||
| 51 | + heading: 'Orders', | ||
| 52 | + items: [ | ||
| 53 | + { to: '/sales-orders', label: 'Sales Orders' }, | ||
| 54 | + { to: '/purchase-orders', label: 'Purchase Orders' }, | ||
| 55 | + ], | ||
| 56 | + }, | ||
| 57 | + { | ||
| 58 | + heading: 'Production', | ||
| 59 | + items: [ | ||
| 60 | + { to: '/work-orders', label: 'Work Orders' }, | ||
| 61 | + { to: '/shop-floor', label: 'Shop Floor' }, | ||
| 62 | + ], | ||
| 63 | + }, | ||
| 64 | + { | ||
| 65 | + heading: 'Finance', | ||
| 66 | + items: [{ to: '/journal-entries', label: 'Journal Entries' }], | ||
| 67 | + }, | ||
| 68 | +] | ||
| 69 | + | ||
| 70 | +export function AppLayout() { | ||
| 71 | + const { username, logout } = useAuth() | ||
| 72 | + const [info, setInfo] = useState<MetaInfo | null>(null) | ||
| 73 | + | ||
| 74 | + useEffect(() => { | ||
| 75 | + meta.info().then(setInfo).catch(() => setInfo(null)) | ||
| 76 | + }, []) | ||
| 77 | + | ||
| 78 | + return ( | ||
| 79 | + <div className="flex min-h-screen bg-slate-50 text-slate-900"> | ||
| 80 | + {/* ─── Sidebar ──────────────────────────────────────────── */} | ||
| 81 | + <aside className="hidden w-60 flex-shrink-0 border-r border-slate-200 bg-white sm:flex sm:flex-col"> | ||
| 82 | + <div className="flex h-14 items-center px-4 border-b border-slate-200"> | ||
| 83 | + <span className="text-lg font-bold text-brand-600">vibe_erp</span> | ||
| 84 | + {info && ( | ||
| 85 | + <span className="ml-2 text-xs text-slate-400"> | ||
| 86 | + {info.implementationVersion} | ||
| 87 | + </span> | ||
| 88 | + )} | ||
| 89 | + </div> | ||
| 90 | + <nav className="flex-1 overflow-y-auto px-3 py-4"> | ||
| 91 | + {NAV.map((group) => ( | ||
| 92 | + <div key={group.heading} className="mb-5"> | ||
| 93 | + <div className="px-2 text-xs font-semibold uppercase tracking-wider text-slate-400"> | ||
| 94 | + {group.heading} | ||
| 95 | + </div> | ||
| 96 | + <ul className="mt-1 space-y-0.5"> | ||
| 97 | + {group.items.map((item) => ( | ||
| 98 | + <li key={item.to}> | ||
| 99 | + <NavLink | ||
| 100 | + to={item.to} | ||
| 101 | + end={item.to === '/'} | ||
| 102 | + className={({ isActive }) => | ||
| 103 | + [ | ||
| 104 | + 'block rounded-md px-2 py-1.5 text-sm transition', | ||
| 105 | + isActive | ||
| 106 | + ? 'bg-brand-50 text-brand-700 font-semibold' | ||
| 107 | + : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', | ||
| 108 | + ].join(' ') | ||
| 109 | + } | ||
| 110 | + > | ||
| 111 | + {item.label} | ||
| 112 | + </NavLink> | ||
| 113 | + </li> | ||
| 114 | + ))} | ||
| 115 | + </ul> | ||
| 116 | + </div> | ||
| 117 | + ))} | ||
| 118 | + </nav> | ||
| 119 | + <div className="border-t border-slate-200 px-3 py-3 text-xs text-slate-400"> | ||
| 120 | + {info?.activeProfiles?.length ? ( | ||
| 121 | + <div>profiles: {info.activeProfiles.join(', ')}</div> | ||
| 122 | + ) : null} | ||
| 123 | + {info?.buildTime ? ( | ||
| 124 | + <div>built: {new Date(info.buildTime).toLocaleString()}</div> | ||
| 125 | + ) : null} | ||
| 126 | + </div> | ||
| 127 | + </aside> | ||
| 128 | + | ||
| 129 | + {/* ─── Main column ─────────────────────────────────────── */} | ||
| 130 | + <div className="flex flex-1 flex-col"> | ||
| 131 | + <header className="flex h-14 items-center justify-between border-b border-slate-200 bg-white px-6 sm:justify-end"> | ||
| 132 | + <span className="text-lg font-bold text-brand-600 sm:hidden">vibe_erp</span> | ||
| 133 | + <div className="flex items-center gap-3"> | ||
| 134 | + <span className="text-sm text-slate-500"> | ||
| 135 | + {username ? ( | ||
| 136 | + <> | ||
| 137 | + Signed in as <span className="font-medium text-slate-700">{username}</span> | ||
| 138 | + </> | ||
| 139 | + ) : ( | ||
| 140 | + 'Signed in' | ||
| 141 | + )} | ||
| 142 | + </span> | ||
| 143 | + <button className="btn-secondary" onClick={logout}> | ||
| 144 | + Logout | ||
| 145 | + </button> | ||
| 146 | + </div> | ||
| 147 | + </header> | ||
| 148 | + <main className="flex-1 overflow-y-auto p-6"> | ||
| 149 | + <Outlet /> | ||
| 150 | + </main> | ||
| 151 | + </div> | ||
| 152 | + </div> | ||
| 153 | + ) | ||
| 154 | +} |
web/src/main.tsx
0 → 100644
| 1 | +import React from 'react' | ||
| 2 | +import ReactDOM from 'react-dom/client' | ||
| 3 | +import { BrowserRouter } from 'react-router-dom' | ||
| 4 | +import App from './App' | ||
| 5 | +import { AuthProvider } from './auth/AuthContext' | ||
| 6 | +import './index.css' | ||
| 7 | + | ||
| 8 | +ReactDOM.createRoot(document.getElementById('root')!).render( | ||
| 9 | + <React.StrictMode> | ||
| 10 | + <BrowserRouter> | ||
| 11 | + <AuthProvider> | ||
| 12 | + <App /> | ||
| 13 | + </AuthProvider> | ||
| 14 | + </BrowserRouter> | ||
| 15 | + </React.StrictMode>, | ||
| 16 | +) |
web/src/pages/BalancesPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { inventory } from '@/api/client' | ||
| 3 | +import type { Location, StockBalance } from '@/types/api' | ||
| 4 | +import { PageHeader } from '@/components/PageHeader' | ||
| 5 | +import { Loading } from '@/components/Loading' | ||
| 6 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 7 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 8 | + | ||
| 9 | +interface Row extends Record<string, unknown> { | ||
| 10 | + id: string | ||
| 11 | + itemCode: string | ||
| 12 | + locationCode: string | ||
| 13 | + quantity: string | number | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +export function BalancesPage() { | ||
| 17 | + const [rows, setRows] = useState<Row[]>([]) | ||
| 18 | + const [error, setError] = useState<Error | null>(null) | ||
| 19 | + const [loading, setLoading] = useState(true) | ||
| 20 | + | ||
| 21 | + useEffect(() => { | ||
| 22 | + Promise.all([inventory.listBalances(), inventory.listLocations()]) | ||
| 23 | + .then(([balances, locs]: [StockBalance[], Location[]]) => { | ||
| 24 | + const byId = new Map(locs.map((l) => [l.id, l.code])) | ||
| 25 | + setRows( | ||
| 26 | + balances.map((b) => ({ | ||
| 27 | + id: b.id, | ||
| 28 | + itemCode: b.itemCode, | ||
| 29 | + locationCode: byId.get(b.locationId) ?? b.locationId, | ||
| 30 | + quantity: b.quantity, | ||
| 31 | + })), | ||
| 32 | + ) | ||
| 33 | + }) | ||
| 34 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 35 | + .finally(() => setLoading(false)) | ||
| 36 | + }, []) | ||
| 37 | + | ||
| 38 | + const columns: Column<Row>[] = [ | ||
| 39 | + { header: 'Item', key: 'itemCode', render: (r) => <span className="font-mono">{r.itemCode}</span> }, | ||
| 40 | + { | ||
| 41 | + header: 'Location', | ||
| 42 | + key: 'locationCode', | ||
| 43 | + render: (r) => <span className="font-mono">{r.locationCode}</span>, | ||
| 44 | + }, | ||
| 45 | + { | ||
| 46 | + header: 'Quantity', | ||
| 47 | + key: 'quantity', | ||
| 48 | + render: (r) => <span className="font-mono tabular-nums">{String(r.quantity)}</span>, | ||
| 49 | + }, | ||
| 50 | + ] | ||
| 51 | + | ||
| 52 | + return ( | ||
| 53 | + <div> | ||
| 54 | + <PageHeader | ||
| 55 | + title="Stock Balances" | ||
| 56 | + subtitle="On-hand quantities per (item, location). Updates atomically with every movement." | ||
| 57 | + /> | ||
| 58 | + {loading && <Loading />} | ||
| 59 | + {error && <ErrorBox error={error} />} | ||
| 60 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 61 | + </div> | ||
| 62 | + ) | ||
| 63 | +} |
web/src/pages/DashboardPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { Link } from 'react-router-dom' | ||
| 3 | +import { | ||
| 4 | + catalog, | ||
| 5 | + finance, | ||
| 6 | + inventory, | ||
| 7 | + partners, | ||
| 8 | + production, | ||
| 9 | + purchaseOrders, | ||
| 10 | + salesOrders, | ||
| 11 | +} from '@/api/client' | ||
| 12 | +import { PageHeader } from '@/components/PageHeader' | ||
| 13 | +import { Loading } from '@/components/Loading' | ||
| 14 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 15 | +import { useAuth } from '@/auth/AuthContext' | ||
| 16 | + | ||
| 17 | +interface DashboardCounts { | ||
| 18 | + items: number | ||
| 19 | + partners: number | ||
| 20 | + locations: number | ||
| 21 | + salesOrders: number | ||
| 22 | + purchaseOrders: number | ||
| 23 | + workOrders: number | ||
| 24 | + journalEntries: number | ||
| 25 | + inProgressWorkOrders: number | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +export function DashboardPage() { | ||
| 29 | + const { username } = useAuth() | ||
| 30 | + const [counts, setCounts] = useState<DashboardCounts | null>(null) | ||
| 31 | + const [error, setError] = useState<Error | null>(null) | ||
| 32 | + const [loading, setLoading] = useState(true) | ||
| 33 | + | ||
| 34 | + useEffect(() => { | ||
| 35 | + let active = true | ||
| 36 | + setLoading(true) | ||
| 37 | + Promise.all([ | ||
| 38 | + catalog.listItems(), | ||
| 39 | + partners.list(), | ||
| 40 | + inventory.listLocations(), | ||
| 41 | + salesOrders.list(), | ||
| 42 | + purchaseOrders.list(), | ||
| 43 | + production.listWorkOrders(), | ||
| 44 | + finance.listJournalEntries(), | ||
| 45 | + production.shopFloor(), | ||
| 46 | + ]) | ||
| 47 | + .then(([items, parts, locs, sos, pos, wos, jes, sf]) => { | ||
| 48 | + if (!active) return | ||
| 49 | + setCounts({ | ||
| 50 | + items: items.length, | ||
| 51 | + partners: parts.length, | ||
| 52 | + locations: locs.length, | ||
| 53 | + salesOrders: sos.length, | ||
| 54 | + purchaseOrders: pos.length, | ||
| 55 | + workOrders: wos.length, | ||
| 56 | + journalEntries: jes.length, | ||
| 57 | + inProgressWorkOrders: sf.length, | ||
| 58 | + }) | ||
| 59 | + }) | ||
| 60 | + .catch((e: unknown) => { | ||
| 61 | + if (active) setError(e instanceof Error ? e : new Error(String(e))) | ||
| 62 | + }) | ||
| 63 | + .finally(() => active && setLoading(false)) | ||
| 64 | + return () => { | ||
| 65 | + active = false | ||
| 66 | + } | ||
| 67 | + }, []) | ||
| 68 | + | ||
| 69 | + return ( | ||
| 70 | + <div> | ||
| 71 | + <PageHeader | ||
| 72 | + title={`Welcome${username ? ', ' + username : ''}`} | ||
| 73 | + subtitle="The framework's buy-make-sell loop, end to end through the same Postgres." | ||
| 74 | + /> | ||
| 75 | + {loading && <Loading />} | ||
| 76 | + {error && <ErrorBox error={error} />} | ||
| 77 | + {counts && ( | ||
| 78 | + <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> | ||
| 79 | + <DashboardCard label="Items" value={counts.items} to="/items" /> | ||
| 80 | + <DashboardCard label="Partners" value={counts.partners} to="/partners" /> | ||
| 81 | + <DashboardCard label="Locations" value={counts.locations} to="/locations" /> | ||
| 82 | + <DashboardCard | ||
| 83 | + label="Work orders in progress" | ||
| 84 | + value={counts.inProgressWorkOrders} | ||
| 85 | + to="/shop-floor" | ||
| 86 | + highlight | ||
| 87 | + /> | ||
| 88 | + <DashboardCard label="Sales orders" value={counts.salesOrders} to="/sales-orders" /> | ||
| 89 | + <DashboardCard | ||
| 90 | + label="Purchase orders" | ||
| 91 | + value={counts.purchaseOrders} | ||
| 92 | + to="/purchase-orders" | ||
| 93 | + /> | ||
| 94 | + <DashboardCard label="Work orders" value={counts.workOrders} to="/work-orders" /> | ||
| 95 | + <DashboardCard | ||
| 96 | + label="Journal entries" | ||
| 97 | + value={counts.journalEntries} | ||
| 98 | + to="/journal-entries" | ||
| 99 | + /> | ||
| 100 | + </div> | ||
| 101 | + )} | ||
| 102 | + <div className="mt-8 card p-5 text-sm text-slate-600"> | ||
| 103 | + <h2 className="mb-2 text-base font-semibold text-slate-800">Try the demo</h2> | ||
| 104 | + <ol className="list-decimal space-y-1 pl-5"> | ||
| 105 | + <li> | ||
| 106 | + Open a <Link to="/sales-orders" className="text-brand-600 hover:underline">sales order</Link>{' '} | ||
| 107 | + in DRAFT, click <span className="font-mono">Confirm</span>. | ||
| 108 | + </li> | ||
| 109 | + <li> | ||
| 110 | + With the same sales order CONFIRMED, click <span className="font-mono">Ship</span>. | ||
| 111 | + The framework atomically debits stock, flips status to SHIPPED, and emits a domain | ||
| 112 | + event. | ||
| 113 | + </li> | ||
| 114 | + <li> | ||
| 115 | + Watch <Link to="/balances" className="text-brand-600 hover:underline">stock balances</Link>{' '} | ||
| 116 | + drop, <Link to="/movements" className="text-brand-600 hover:underline">movements</Link>{' '} | ||
| 117 | + grow by one row, and{' '} | ||
| 118 | + <Link to="/journal-entries" className="text-brand-600 hover:underline"> | ||
| 119 | + journal entries | ||
| 120 | + </Link>{' '} | ||
| 121 | + settle from POSTED to SETTLED — all from the cross-PBC event subscriber in pbc-finance. | ||
| 122 | + </li> | ||
| 123 | + </ol> | ||
| 124 | + </div> | ||
| 125 | + </div> | ||
| 126 | + ) | ||
| 127 | +} | ||
| 128 | + | ||
| 129 | +function DashboardCard({ | ||
| 130 | + label, | ||
| 131 | + value, | ||
| 132 | + to, | ||
| 133 | + highlight = false, | ||
| 134 | +}: { | ||
| 135 | + label: string | ||
| 136 | + value: number | ||
| 137 | + to: string | ||
| 138 | + highlight?: boolean | ||
| 139 | +}) { | ||
| 140 | + return ( | ||
| 141 | + <Link | ||
| 142 | + to={to} | ||
| 143 | + className={[ | ||
| 144 | + 'card flex flex-col gap-1 p-5 transition hover:shadow-md', | ||
| 145 | + highlight ? 'ring-2 ring-brand-200' : '', | ||
| 146 | + ].join(' ')} | ||
| 147 | + > | ||
| 148 | + <div className="text-xs font-semibold uppercase tracking-wide text-slate-500">{label}</div> | ||
| 149 | + <div className="text-3xl font-bold text-slate-900">{value}</div> | ||
| 150 | + </Link> | ||
| 151 | + ) | ||
| 152 | +} |
web/src/pages/ItemsPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { catalog } from '@/api/client' | ||
| 3 | +import type { Item } from '@/types/api' | ||
| 4 | +import { PageHeader } from '@/components/PageHeader' | ||
| 5 | +import { Loading } from '@/components/Loading' | ||
| 6 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 7 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 8 | + | ||
| 9 | +export function ItemsPage() { | ||
| 10 | + const [rows, setRows] = useState<Item[]>([]) | ||
| 11 | + const [error, setError] = useState<Error | null>(null) | ||
| 12 | + const [loading, setLoading] = useState(true) | ||
| 13 | + | ||
| 14 | + useEffect(() => { | ||
| 15 | + catalog | ||
| 16 | + .listItems() | ||
| 17 | + .then(setRows) | ||
| 18 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 19 | + .finally(() => setLoading(false)) | ||
| 20 | + }, []) | ||
| 21 | + | ||
| 22 | + const columns: Column<Item>[] = [ | ||
| 23 | + { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> }, | ||
| 24 | + { header: 'Name', key: 'name' }, | ||
| 25 | + { header: 'Type', key: 'itemType' }, | ||
| 26 | + { header: 'UoM', key: 'baseUomCode', render: (r) => <span className="font-mono">{r.baseUomCode}</span> }, | ||
| 27 | + { | ||
| 28 | + header: 'Active', | ||
| 29 | + key: 'active', | ||
| 30 | + render: (r) => (r.active ? <span className="text-emerald-600">●</span> : <span className="text-slate-300">●</span>), | ||
| 31 | + }, | ||
| 32 | + ] | ||
| 33 | + | ||
| 34 | + return ( | ||
| 35 | + <div> | ||
| 36 | + <PageHeader | ||
| 37 | + title="Items" | ||
| 38 | + subtitle="Catalog of items the framework can transact: raw materials, components, finished goods, services." | ||
| 39 | + /> | ||
| 40 | + {loading && <Loading />} | ||
| 41 | + {error && <ErrorBox error={error} />} | ||
| 42 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 43 | + </div> | ||
| 44 | + ) | ||
| 45 | +} |
web/src/pages/JournalEntriesPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { finance } from '@/api/client' | ||
| 3 | +import type { JournalEntry } from '@/types/api' | ||
| 4 | +import { PageHeader } from '@/components/PageHeader' | ||
| 5 | +import { Loading } from '@/components/Loading' | ||
| 6 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 7 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 8 | +import { StatusBadge } from '@/components/StatusBadge' | ||
| 9 | + | ||
| 10 | +export function JournalEntriesPage() { | ||
| 11 | + const [rows, setRows] = useState<JournalEntry[]>([]) | ||
| 12 | + const [error, setError] = useState<Error | null>(null) | ||
| 13 | + const [loading, setLoading] = useState(true) | ||
| 14 | + | ||
| 15 | + useEffect(() => { | ||
| 16 | + finance | ||
| 17 | + .listJournalEntries() | ||
| 18 | + .then((rs) => | ||
| 19 | + setRows([...rs].sort((a, b) => (b.postedAt ?? '').localeCompare(a.postedAt ?? ''))), | ||
| 20 | + ) | ||
| 21 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 22 | + .finally(() => setLoading(false)) | ||
| 23 | + }, []) | ||
| 24 | + | ||
| 25 | + const columns: Column<JournalEntry>[] = [ | ||
| 26 | + { | ||
| 27 | + header: 'Posted', | ||
| 28 | + key: 'postedAt', | ||
| 29 | + render: (r) => (r.postedAt ? new Date(r.postedAt).toLocaleString() : '—'), | ||
| 30 | + }, | ||
| 31 | + { header: 'Code', key: 'code', render: (r) => <span className="font-mono text-xs">{r.code}</span> }, | ||
| 32 | + { header: 'Type', key: 'type' }, | ||
| 33 | + { header: 'Status', key: 'status', render: (r) => <StatusBadge status={r.status} /> }, | ||
| 34 | + { header: 'Order', key: 'orderCode', render: (r) => <span className="font-mono">{r.orderCode}</span> }, | ||
| 35 | + { header: 'Partner', key: 'partnerCode', render: (r) => <span className="font-mono">{r.partnerCode}</span> }, | ||
| 36 | + { | ||
| 37 | + header: 'Amount', | ||
| 38 | + key: 'amount', | ||
| 39 | + render: (r) => ( | ||
| 40 | + <span className="font-mono tabular-nums"> | ||
| 41 | + {Number(r.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })}{' '} | ||
| 42 | + {r.currencyCode} | ||
| 43 | + </span> | ||
| 44 | + ), | ||
| 45 | + }, | ||
| 46 | + ] | ||
| 47 | + | ||
| 48 | + return ( | ||
| 49 | + <div> | ||
| 50 | + <PageHeader | ||
| 51 | + title="Journal Entries" | ||
| 52 | + subtitle="AR / AP entries created reactively by pbc-finance from order lifecycle events." | ||
| 53 | + /> | ||
| 54 | + {loading && <Loading />} | ||
| 55 | + {error && <ErrorBox error={error} />} | ||
| 56 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 57 | + </div> | ||
| 58 | + ) | ||
| 59 | +} |
web/src/pages/LocationsPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { inventory } from '@/api/client' | ||
| 3 | +import type { Location } from '@/types/api' | ||
| 4 | +import { PageHeader } from '@/components/PageHeader' | ||
| 5 | +import { Loading } from '@/components/Loading' | ||
| 6 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 7 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 8 | + | ||
| 9 | +export function LocationsPage() { | ||
| 10 | + const [rows, setRows] = useState<Location[]>([]) | ||
| 11 | + const [error, setError] = useState<Error | null>(null) | ||
| 12 | + const [loading, setLoading] = useState(true) | ||
| 13 | + | ||
| 14 | + useEffect(() => { | ||
| 15 | + inventory | ||
| 16 | + .listLocations() | ||
| 17 | + .then(setRows) | ||
| 18 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 19 | + .finally(() => setLoading(false)) | ||
| 20 | + }, []) | ||
| 21 | + | ||
| 22 | + const columns: Column<Location>[] = [ | ||
| 23 | + { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> }, | ||
| 24 | + { header: 'Name', key: 'name' }, | ||
| 25 | + { header: 'Type', key: 'type' }, | ||
| 26 | + { | ||
| 27 | + header: 'Active', | ||
| 28 | + key: 'active', | ||
| 29 | + render: (r) => (r.active ? <span className="text-emerald-600">●</span> : <span className="text-slate-300">●</span>), | ||
| 30 | + }, | ||
| 31 | + ] | ||
| 32 | + | ||
| 33 | + return ( | ||
| 34 | + <div> | ||
| 35 | + <PageHeader title="Locations" subtitle="Warehouses, bins, and virtual locations the inventory PBC tracks." /> | ||
| 36 | + {loading && <Loading />} | ||
| 37 | + {error && <ErrorBox error={error} />} | ||
| 38 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 39 | + </div> | ||
| 40 | + ) | ||
| 41 | +} |
web/src/pages/LoginPage.tsx
0 → 100644
| 1 | +import { useEffect, useState, type FormEvent } from 'react' | ||
| 2 | +import { useLocation, useNavigate } from 'react-router-dom' | ||
| 3 | +import { useAuth } from '@/auth/AuthContext' | ||
| 4 | +import { meta } from '@/api/client' | ||
| 5 | +import type { MetaInfo } from '@/types/api' | ||
| 6 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 7 | + | ||
| 8 | +interface LocationState { | ||
| 9 | + from?: string | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +export function LoginPage() { | ||
| 13 | + const { login, token, loading } = useAuth() | ||
| 14 | + const navigate = useNavigate() | ||
| 15 | + const location = useLocation() | ||
| 16 | + const [username, setUsername] = useState('admin') | ||
| 17 | + const [password, setPassword] = useState('') | ||
| 18 | + const [error, setError] = useState<Error | null>(null) | ||
| 19 | + const [info, setInfo] = useState<MetaInfo | null>(null) | ||
| 20 | + | ||
| 21 | + useEffect(() => { | ||
| 22 | + meta.info().then(setInfo).catch(() => setInfo(null)) | ||
| 23 | + }, []) | ||
| 24 | + | ||
| 25 | + // If already logged in (e.g. nav back to /login), bounce to dashboard. | ||
| 26 | + useEffect(() => { | ||
| 27 | + if (token) { | ||
| 28 | + const dest = (location.state as LocationState | null)?.from ?? '/' | ||
| 29 | + navigate(dest, { replace: true }) | ||
| 30 | + } | ||
| 31 | + }, [token, navigate, location.state]) | ||
| 32 | + | ||
| 33 | + const onSubmit = async (e: FormEvent) => { | ||
| 34 | + e.preventDefault() | ||
| 35 | + setError(null) | ||
| 36 | + try { | ||
| 37 | + await login(username, password) | ||
| 38 | + // Effect above handles the redirect; nothing else to do here. | ||
| 39 | + } catch (err: unknown) { | ||
| 40 | + setError(err instanceof Error ? err : new Error(String(err))) | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + return ( | ||
| 45 | + <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-100 to-brand-50 p-6"> | ||
| 46 | + <div className="w-full max-w-md"> | ||
| 47 | + <div className="mb-6 text-center"> | ||
| 48 | + <h1 className="text-3xl font-bold text-brand-600">vibe_erp</h1> | ||
| 49 | + <p className="mt-1 text-sm text-slate-500"> | ||
| 50 | + Composable ERP framework for the printing industry | ||
| 51 | + </p> | ||
| 52 | + </div> | ||
| 53 | + <div className="card p-6"> | ||
| 54 | + <form onSubmit={onSubmit} className="space-y-4"> | ||
| 55 | + <div> | ||
| 56 | + <label className="block text-sm font-medium text-slate-700">Username</label> | ||
| 57 | + <input | ||
| 58 | + type="text" | ||
| 59 | + value={username} | ||
| 60 | + onChange={(e) => setUsername(e.target.value)} | ||
| 61 | + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-brand-500 focus:ring-brand-500" | ||
| 62 | + required | ||
| 63 | + autoFocus | ||
| 64 | + /> | ||
| 65 | + </div> | ||
| 66 | + <div> | ||
| 67 | + <label className="block text-sm font-medium text-slate-700">Password</label> | ||
| 68 | + <input | ||
| 69 | + type="password" | ||
| 70 | + value={password} | ||
| 71 | + onChange={(e) => setPassword(e.target.value)} | ||
| 72 | + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-brand-500 focus:ring-brand-500" | ||
| 73 | + required | ||
| 74 | + /> | ||
| 75 | + <p className="mt-1 text-xs text-slate-400"> | ||
| 76 | + The bootstrap admin password is printed to the application boot log on first start. | ||
| 77 | + </p> | ||
| 78 | + </div> | ||
| 79 | + {error ? <ErrorBox error={error} /> : null} | ||
| 80 | + <button type="submit" className="btn-primary w-full" disabled={loading}> | ||
| 81 | + {loading ? 'Signing in…' : 'Sign in'} | ||
| 82 | + </button> | ||
| 83 | + </form> | ||
| 84 | + </div> | ||
| 85 | + <p className="mt-4 text-center text-xs text-slate-400"> | ||
| 86 | + {info ? ( | ||
| 87 | + <> | ||
| 88 | + Connected to <span className="font-mono">{info.name}</span>{' '} | ||
| 89 | + <span className="font-mono">{info.implementationVersion}</span> | ||
| 90 | + </> | ||
| 91 | + ) : ( | ||
| 92 | + 'Connecting…' | ||
| 93 | + )} | ||
| 94 | + </p> | ||
| 95 | + </div> | ||
| 96 | + </div> | ||
| 97 | + ) | ||
| 98 | +} |
web/src/pages/MovementsPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { inventory } from '@/api/client' | ||
| 3 | +import type { Location, StockMovement } from '@/types/api' | ||
| 4 | +import { PageHeader } from '@/components/PageHeader' | ||
| 5 | +import { Loading } from '@/components/Loading' | ||
| 6 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 7 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 8 | + | ||
| 9 | +interface Row extends Record<string, unknown> { | ||
| 10 | + id: string | ||
| 11 | + occurredAt: string | ||
| 12 | + itemCode: string | ||
| 13 | + locationCode: string | ||
| 14 | + delta: string | number | ||
| 15 | + reason: string | ||
| 16 | + reference: string | null | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +export function MovementsPage() { | ||
| 20 | + const [rows, setRows] = useState<Row[]>([]) | ||
| 21 | + const [error, setError] = useState<Error | null>(null) | ||
| 22 | + const [loading, setLoading] = useState(true) | ||
| 23 | + | ||
| 24 | + useEffect(() => { | ||
| 25 | + Promise.all([inventory.listMovements(), inventory.listLocations()]) | ||
| 26 | + .then(([moves, locs]: [StockMovement[], Location[]]) => { | ||
| 27 | + const byId = new Map(locs.map((l) => [l.id, l.code])) | ||
| 28 | + const sorted = [...moves].sort((a, b) => | ||
| 29 | + (b.occurredAt ?? '').localeCompare(a.occurredAt ?? ''), | ||
| 30 | + ) | ||
| 31 | + setRows( | ||
| 32 | + sorted.map((m) => ({ | ||
| 33 | + id: m.id, | ||
| 34 | + occurredAt: m.occurredAt, | ||
| 35 | + itemCode: m.itemCode, | ||
| 36 | + locationCode: byId.get(m.locationId) ?? m.locationId, | ||
| 37 | + delta: m.delta, | ||
| 38 | + reason: m.reason, | ||
| 39 | + reference: m.reference, | ||
| 40 | + })), | ||
| 41 | + ) | ||
| 42 | + }) | ||
| 43 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 44 | + .finally(() => setLoading(false)) | ||
| 45 | + }, []) | ||
| 46 | + | ||
| 47 | + const columns: Column<Row>[] = [ | ||
| 48 | + { | ||
| 49 | + header: 'Occurred', | ||
| 50 | + key: 'occurredAt', | ||
| 51 | + render: (r) => | ||
| 52 | + r.occurredAt ? new Date(r.occurredAt).toLocaleString() : '—', | ||
| 53 | + }, | ||
| 54 | + { header: 'Item', key: 'itemCode', render: (r) => <span className="font-mono">{r.itemCode}</span> }, | ||
| 55 | + { header: 'Location', key: 'locationCode', render: (r) => <span className="font-mono">{r.locationCode}</span> }, | ||
| 56 | + { | ||
| 57 | + header: 'Δ', | ||
| 58 | + key: 'delta', | ||
| 59 | + render: (r) => { | ||
| 60 | + const n = Number(r.delta) | ||
| 61 | + const cls = n >= 0 ? 'text-emerald-600' : 'text-rose-600' | ||
| 62 | + return <span className={`font-mono tabular-nums ${cls}`}>{String(r.delta)}</span> | ||
| 63 | + }, | ||
| 64 | + }, | ||
| 65 | + { header: 'Reason', key: 'reason' }, | ||
| 66 | + { header: 'Reference', key: 'reference', render: (r) => r.reference ?? '—' }, | ||
| 67 | + ] | ||
| 68 | + | ||
| 69 | + return ( | ||
| 70 | + <div> | ||
| 71 | + <PageHeader | ||
| 72 | + title="Stock Movements" | ||
| 73 | + subtitle="Append-only ledger of every change to inventory. Most-recent first." | ||
| 74 | + /> | ||
| 75 | + {loading && <Loading />} | ||
| 76 | + {error && <ErrorBox error={error} />} | ||
| 77 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 78 | + </div> | ||
| 79 | + ) | ||
| 80 | +} |
web/src/pages/PartnersPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { partners } from '@/api/client' | ||
| 3 | +import type { Partner } from '@/types/api' | ||
| 4 | +import { PageHeader } from '@/components/PageHeader' | ||
| 5 | +import { Loading } from '@/components/Loading' | ||
| 6 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 7 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 8 | + | ||
| 9 | +export function PartnersPage() { | ||
| 10 | + const [rows, setRows] = useState<Partner[]>([]) | ||
| 11 | + const [error, setError] = useState<Error | null>(null) | ||
| 12 | + const [loading, setLoading] = useState(true) | ||
| 13 | + | ||
| 14 | + useEffect(() => { | ||
| 15 | + partners | ||
| 16 | + .list() | ||
| 17 | + .then(setRows) | ||
| 18 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 19 | + .finally(() => setLoading(false)) | ||
| 20 | + }, []) | ||
| 21 | + | ||
| 22 | + const columns: Column<Partner>[] = [ | ||
| 23 | + { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> }, | ||
| 24 | + { header: 'Name', key: 'name' }, | ||
| 25 | + { header: 'Type', key: 'type' }, | ||
| 26 | + { header: 'Email', key: 'email', render: (r) => r.email ?? '—' }, | ||
| 27 | + { header: 'Phone', key: 'phone', render: (r) => r.phone ?? '—' }, | ||
| 28 | + { | ||
| 29 | + header: 'Active', | ||
| 30 | + key: 'active', | ||
| 31 | + render: (r) => (r.active ? <span className="text-emerald-600">●</span> : <span className="text-slate-300">●</span>), | ||
| 32 | + }, | ||
| 33 | + ] | ||
| 34 | + | ||
| 35 | + return ( | ||
| 36 | + <div> | ||
| 37 | + <PageHeader | ||
| 38 | + title="Partners" | ||
| 39 | + subtitle="Customers, suppliers, and partners that play both roles. Seeded by the demo." | ||
| 40 | + /> | ||
| 41 | + {loading && <Loading />} | ||
| 42 | + {error && <ErrorBox error={error} />} | ||
| 43 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 44 | + </div> | ||
| 45 | + ) | ||
| 46 | +} |
web/src/pages/PurchaseOrderDetailPage.tsx
0 → 100644
| 1 | +// Purchase-order detail screen — symmetric to SalesOrderDetailPage. | ||
| 2 | +// Confirm a DRAFT, receive a CONFIRMED into a warehouse, or cancel | ||
| 3 | +// either. Each action surfaces the corresponding stock movement | ||
| 4 | +// (PURCHASE_RECEIPT) and pbc-finance journal entry (AP, POSTED → | ||
| 5 | +// SETTLED on receive) inline. | ||
| 6 | + | ||
| 7 | +import { useCallback, useEffect, useState } from 'react' | ||
| 8 | +import { useNavigate, useParams } from 'react-router-dom' | ||
| 9 | +import { finance, inventory, purchaseOrders } from '@/api/client' | ||
| 10 | +import type { JournalEntry, Location, PurchaseOrder, StockMovement } from '@/types/api' | ||
| 11 | +import { PageHeader } from '@/components/PageHeader' | ||
| 12 | +import { Loading } from '@/components/Loading' | ||
| 13 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 14 | +import { StatusBadge } from '@/components/StatusBadge' | ||
| 15 | + | ||
| 16 | +export function PurchaseOrderDetailPage() { | ||
| 17 | + const { id = '' } = useParams<{ id: string }>() | ||
| 18 | + const navigate = useNavigate() | ||
| 19 | + const [order, setOrder] = useState<PurchaseOrder | null>(null) | ||
| 20 | + const [locations, setLocations] = useState<Location[]>([]) | ||
| 21 | + const [receivingLocation, setReceivingLocation] = useState<string>('') | ||
| 22 | + const [movements, setMovements] = useState<StockMovement[]>([]) | ||
| 23 | + const [journalEntries, setJournalEntries] = useState<JournalEntry[]>([]) | ||
| 24 | + const [loading, setLoading] = useState(true) | ||
| 25 | + const [acting, setActing] = useState(false) | ||
| 26 | + const [error, setError] = useState<Error | null>(null) | ||
| 27 | + const [actionMessage, setActionMessage] = useState<string | null>(null) | ||
| 28 | + | ||
| 29 | + const reloadSideEffects = useCallback(async (orderCode: string) => { | ||
| 30 | + const [ms, js] = await Promise.all([ | ||
| 31 | + inventory.listMovements(), | ||
| 32 | + finance.listJournalEntries(), | ||
| 33 | + ]) | ||
| 34 | + setMovements(ms.filter((m) => m.reference?.includes(orderCode) ?? false)) | ||
| 35 | + setJournalEntries(js.filter((j) => j.orderCode === orderCode)) | ||
| 36 | + }, []) | ||
| 37 | + | ||
| 38 | + useEffect(() => { | ||
| 39 | + let active = true | ||
| 40 | + setLoading(true) | ||
| 41 | + Promise.all([purchaseOrders.get(id), inventory.listLocations()]) | ||
| 42 | + .then(async ([o, locs]: [PurchaseOrder, Location[]]) => { | ||
| 43 | + if (!active) return | ||
| 44 | + setOrder(o) | ||
| 45 | + setLocations(locs.filter((l) => l.active)) | ||
| 46 | + const firstWarehouse = locs.find((l) => l.active && l.type === 'WAREHOUSE') | ||
| 47 | + setReceivingLocation(firstWarehouse?.code ?? locs[0]?.code ?? '') | ||
| 48 | + await reloadSideEffects(o.code) | ||
| 49 | + }) | ||
| 50 | + .catch((e: unknown) => { | ||
| 51 | + if (active) setError(e instanceof Error ? e : new Error(String(e))) | ||
| 52 | + }) | ||
| 53 | + .finally(() => active && setLoading(false)) | ||
| 54 | + return () => { | ||
| 55 | + active = false | ||
| 56 | + } | ||
| 57 | + }, [id, reloadSideEffects]) | ||
| 58 | + | ||
| 59 | + if (loading) return <Loading /> | ||
| 60 | + if (error) return <ErrorBox error={error} /> | ||
| 61 | + if (!order) return <ErrorBox error="Purchase order not found" /> | ||
| 62 | + | ||
| 63 | + const onConfirm = async () => { | ||
| 64 | + setActing(true) | ||
| 65 | + setError(null) | ||
| 66 | + setActionMessage(null) | ||
| 67 | + try { | ||
| 68 | + const updated = await purchaseOrders.confirm(order.id) | ||
| 69 | + setOrder(updated) | ||
| 70 | + await reloadSideEffects(updated.code) | ||
| 71 | + setActionMessage('Confirmed. pbc-finance has posted an AP journal entry.') | ||
| 72 | + } catch (e: unknown) { | ||
| 73 | + setError(e instanceof Error ? e : new Error(String(e))) | ||
| 74 | + } finally { | ||
| 75 | + setActing(false) | ||
| 76 | + } | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + const onReceive = async () => { | ||
| 80 | + if (!receivingLocation) { | ||
| 81 | + setError(new Error('Pick a receiving location first.')) | ||
| 82 | + return | ||
| 83 | + } | ||
| 84 | + setActing(true) | ||
| 85 | + setError(null) | ||
| 86 | + setActionMessage(null) | ||
| 87 | + try { | ||
| 88 | + const updated = await purchaseOrders.receive(order.id, receivingLocation) | ||
| 89 | + setOrder(updated) | ||
| 90 | + await reloadSideEffects(updated.code) | ||
| 91 | + setActionMessage( | ||
| 92 | + `Received into ${receivingLocation}. Stock credited, journal entry settled.`, | ||
| 93 | + ) | ||
| 94 | + } catch (e: unknown) { | ||
| 95 | + setError(e instanceof Error ? e : new Error(String(e))) | ||
| 96 | + } finally { | ||
| 97 | + setActing(false) | ||
| 98 | + } | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + const onCancel = async () => { | ||
| 102 | + setActing(true) | ||
| 103 | + setError(null) | ||
| 104 | + setActionMessage(null) | ||
| 105 | + try { | ||
| 106 | + const updated = await purchaseOrders.cancel(order.id) | ||
| 107 | + setOrder(updated) | ||
| 108 | + await reloadSideEffects(updated.code) | ||
| 109 | + setActionMessage('Cancelled. pbc-finance has reversed any open journal entry.') | ||
| 110 | + } catch (e: unknown) { | ||
| 111 | + setError(e instanceof Error ? e : new Error(String(e))) | ||
| 112 | + } finally { | ||
| 113 | + setActing(false) | ||
| 114 | + } | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + const canConfirm = order.status === 'DRAFT' | ||
| 118 | + const canReceive = order.status === 'CONFIRMED' | ||
| 119 | + const canCancel = order.status === 'DRAFT' || order.status === 'CONFIRMED' | ||
| 120 | + | ||
| 121 | + return ( | ||
| 122 | + <div> | ||
| 123 | + <PageHeader | ||
| 124 | + title={`Purchase Order ${order.code}`} | ||
| 125 | + subtitle={`Supplier ${order.partnerCode} · ${order.orderDate} · ${order.currencyCode}`} | ||
| 126 | + actions={ | ||
| 127 | + <button className="btn-secondary" onClick={() => navigate('/purchase-orders')}> | ||
| 128 | + ← Back | ||
| 129 | + </button> | ||
| 130 | + } | ||
| 131 | + /> | ||
| 132 | + | ||
| 133 | + <div className="card mb-6 p-5"> | ||
| 134 | + <div className="flex flex-wrap items-center justify-between gap-4"> | ||
| 135 | + <div className="flex items-center gap-3"> | ||
| 136 | + <span className="text-sm text-slate-500">Status:</span> | ||
| 137 | + <StatusBadge status={order.status} /> | ||
| 138 | + </div> | ||
| 139 | + <div className="text-right"> | ||
| 140 | + <div className="text-xs uppercase text-slate-400">Total</div> | ||
| 141 | + <div className="font-mono text-xl font-semibold"> | ||
| 142 | + {Number(order.totalAmount).toLocaleString(undefined, { | ||
| 143 | + minimumFractionDigits: 2, | ||
| 144 | + })}{' '} | ||
| 145 | + {order.currencyCode} | ||
| 146 | + </div> | ||
| 147 | + </div> | ||
| 148 | + </div> | ||
| 149 | + {actionMessage && ( | ||
| 150 | + <div className="mt-4 rounded-md border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm text-emerald-800"> | ||
| 151 | + {actionMessage} | ||
| 152 | + </div> | ||
| 153 | + )} | ||
| 154 | + </div> | ||
| 155 | + | ||
| 156 | + <div className="card mb-6 p-5"> | ||
| 157 | + <h2 className="mb-3 text-base font-semibold text-slate-800">Actions</h2> | ||
| 158 | + <div className="flex flex-wrap items-center gap-3"> | ||
| 159 | + <button className="btn-primary" disabled={!canConfirm || acting} onClick={onConfirm}> | ||
| 160 | + Confirm | ||
| 161 | + </button> | ||
| 162 | + <div className="flex items-center gap-2"> | ||
| 163 | + <select | ||
| 164 | + className="rounded-md border border-slate-300 px-2 py-1 text-sm shadow-sm focus:border-brand-500 focus:ring-brand-500" | ||
| 165 | + value={receivingLocation} | ||
| 166 | + onChange={(e) => setReceivingLocation(e.target.value)} | ||
| 167 | + disabled={!canReceive || acting} | ||
| 168 | + > | ||
| 169 | + {locations.map((l) => ( | ||
| 170 | + <option key={l.id} value={l.code}> | ||
| 171 | + {l.code} — {l.name} | ||
| 172 | + </option> | ||
| 173 | + ))} | ||
| 174 | + </select> | ||
| 175 | + <button className="btn-primary" disabled={!canReceive || acting} onClick={onReceive}> | ||
| 176 | + Receive | ||
| 177 | + </button> | ||
| 178 | + </div> | ||
| 179 | + <button className="btn-danger" disabled={!canCancel || acting} onClick={onCancel}> | ||
| 180 | + Cancel | ||
| 181 | + </button> | ||
| 182 | + </div> | ||
| 183 | + </div> | ||
| 184 | + | ||
| 185 | + <div className="card mb-6"> | ||
| 186 | + <div className="border-b border-slate-200 px-5 py-3"> | ||
| 187 | + <h2 className="text-base font-semibold text-slate-800">Lines</h2> | ||
| 188 | + </div> | ||
| 189 | + <table className="table-base"> | ||
| 190 | + <thead className="bg-slate-50"> | ||
| 191 | + <tr> | ||
| 192 | + <th>#</th> | ||
| 193 | + <th>Item</th> | ||
| 194 | + <th>Qty</th> | ||
| 195 | + <th>Unit price</th> | ||
| 196 | + <th>Line total</th> | ||
| 197 | + </tr> | ||
| 198 | + </thead> | ||
| 199 | + <tbody className="divide-y divide-slate-100"> | ||
| 200 | + {order.lines.map((l) => ( | ||
| 201 | + <tr key={l.id}> | ||
| 202 | + <td>{l.lineNo}</td> | ||
| 203 | + <td className="font-mono">{l.itemCode}</td> | ||
| 204 | + <td className="font-mono tabular-nums">{String(l.quantity)}</td> | ||
| 205 | + <td className="font-mono tabular-nums"> | ||
| 206 | + {Number(l.unitPrice).toLocaleString(undefined, { | ||
| 207 | + minimumFractionDigits: 2, | ||
| 208 | + })}{' '} | ||
| 209 | + {l.currencyCode} | ||
| 210 | + </td> | ||
| 211 | + <td className="font-mono tabular-nums"> | ||
| 212 | + {Number(l.lineTotal).toLocaleString(undefined, { | ||
| 213 | + minimumFractionDigits: 2, | ||
| 214 | + })} | ||
| 215 | + </td> | ||
| 216 | + </tr> | ||
| 217 | + ))} | ||
| 218 | + </tbody> | ||
| 219 | + </table> | ||
| 220 | + </div> | ||
| 221 | + | ||
| 222 | + <div className="grid gap-6 md:grid-cols-2"> | ||
| 223 | + <div className="card"> | ||
| 224 | + <div className="border-b border-slate-200 px-5 py-3"> | ||
| 225 | + <h2 className="text-base font-semibold text-slate-800">Inventory movements</h2> | ||
| 226 | + </div> | ||
| 227 | + {movements.length === 0 ? ( | ||
| 228 | + <div className="p-5 text-sm text-slate-400">No movements yet.</div> | ||
| 229 | + ) : ( | ||
| 230 | + <table className="table-base"> | ||
| 231 | + <thead className="bg-slate-50"> | ||
| 232 | + <tr> | ||
| 233 | + <th>Item</th> | ||
| 234 | + <th>Δ</th> | ||
| 235 | + <th>Reason</th> | ||
| 236 | + <th>Reference</th> | ||
| 237 | + </tr> | ||
| 238 | + </thead> | ||
| 239 | + <tbody className="divide-y divide-slate-100"> | ||
| 240 | + {movements.map((m) => ( | ||
| 241 | + <tr key={m.id}> | ||
| 242 | + <td className="font-mono">{m.itemCode}</td> | ||
| 243 | + <td className="font-mono tabular-nums text-emerald-600">{String(m.delta)}</td> | ||
| 244 | + <td>{m.reason}</td> | ||
| 245 | + <td className="font-mono text-xs text-slate-500">{m.reference ?? '—'}</td> | ||
| 246 | + </tr> | ||
| 247 | + ))} | ||
| 248 | + </tbody> | ||
| 249 | + </table> | ||
| 250 | + )} | ||
| 251 | + </div> | ||
| 252 | + <div className="card"> | ||
| 253 | + <div className="border-b border-slate-200 px-5 py-3"> | ||
| 254 | + <h2 className="text-base font-semibold text-slate-800">Journal entries</h2> | ||
| 255 | + </div> | ||
| 256 | + {journalEntries.length === 0 ? ( | ||
| 257 | + <div className="p-5 text-sm text-slate-400">No entries yet.</div> | ||
| 258 | + ) : ( | ||
| 259 | + <table className="table-base"> | ||
| 260 | + <thead className="bg-slate-50"> | ||
| 261 | + <tr> | ||
| 262 | + <th>Code</th> | ||
| 263 | + <th>Type</th> | ||
| 264 | + <th>Status</th> | ||
| 265 | + <th>Amount</th> | ||
| 266 | + </tr> | ||
| 267 | + </thead> | ||
| 268 | + <tbody className="divide-y divide-slate-100"> | ||
| 269 | + {journalEntries.map((j) => ( | ||
| 270 | + <tr key={j.id}> | ||
| 271 | + <td className="font-mono text-xs">{j.code}</td> | ||
| 272 | + <td>{j.type}</td> | ||
| 273 | + <td> | ||
| 274 | + <StatusBadge status={j.status} /> | ||
| 275 | + </td> | ||
| 276 | + <td className="font-mono tabular-nums"> | ||
| 277 | + {Number(j.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })}{' '} | ||
| 278 | + {j.currencyCode} | ||
| 279 | + </td> | ||
| 280 | + </tr> | ||
| 281 | + ))} | ||
| 282 | + </tbody> | ||
| 283 | + </table> | ||
| 284 | + )} | ||
| 285 | + </div> | ||
| 286 | + </div> | ||
| 287 | + </div> | ||
| 288 | + ) | ||
| 289 | +} |
web/src/pages/PurchaseOrdersPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { Link } from 'react-router-dom' | ||
| 3 | +import { purchaseOrders } from '@/api/client' | ||
| 4 | +import type { PurchaseOrder } from '@/types/api' | ||
| 5 | +import { PageHeader } from '@/components/PageHeader' | ||
| 6 | +import { Loading } from '@/components/Loading' | ||
| 7 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 8 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 9 | +import { StatusBadge } from '@/components/StatusBadge' | ||
| 10 | + | ||
| 11 | +export function PurchaseOrdersPage() { | ||
| 12 | + const [rows, setRows] = useState<PurchaseOrder[]>([]) | ||
| 13 | + const [error, setError] = useState<Error | null>(null) | ||
| 14 | + const [loading, setLoading] = useState(true) | ||
| 15 | + | ||
| 16 | + useEffect(() => { | ||
| 17 | + purchaseOrders | ||
| 18 | + .list() | ||
| 19 | + .then((rs) => setRows([...rs].sort((a, b) => a.code.localeCompare(b.code)))) | ||
| 20 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 21 | + .finally(() => setLoading(false)) | ||
| 22 | + }, []) | ||
| 23 | + | ||
| 24 | + const columns: Column<PurchaseOrder>[] = [ | ||
| 25 | + { | ||
| 26 | + header: 'Code', | ||
| 27 | + key: 'code', | ||
| 28 | + render: (r) => ( | ||
| 29 | + <Link to={`/purchase-orders/${r.id}`} className="font-mono text-brand-600 hover:underline"> | ||
| 30 | + {r.code} | ||
| 31 | + </Link> | ||
| 32 | + ), | ||
| 33 | + }, | ||
| 34 | + { header: 'Supplier', key: 'partnerCode', render: (r) => <span className="font-mono">{r.partnerCode}</span> }, | ||
| 35 | + { header: 'Order date', key: 'orderDate' }, | ||
| 36 | + { header: 'Expected', key: 'expectedDate', render: (r) => r.expectedDate ?? '—' }, | ||
| 37 | + { header: 'Status', key: 'status', render: (r) => <StatusBadge status={r.status} /> }, | ||
| 38 | + { | ||
| 39 | + header: 'Total', | ||
| 40 | + key: 'totalAmount', | ||
| 41 | + render: (r) => ( | ||
| 42 | + <span className="font-mono tabular-nums"> | ||
| 43 | + {Number(r.totalAmount).toLocaleString(undefined, { | ||
| 44 | + minimumFractionDigits: 2, | ||
| 45 | + })}{' '} | ||
| 46 | + {r.currencyCode} | ||
| 47 | + </span> | ||
| 48 | + ), | ||
| 49 | + }, | ||
| 50 | + ] | ||
| 51 | + | ||
| 52 | + return ( | ||
| 53 | + <div> | ||
| 54 | + <PageHeader title="Purchase Orders" subtitle="Supplier-facing orders. Click a code to drill in." /> | ||
| 55 | + {loading && <Loading />} | ||
| 56 | + {error && <ErrorBox error={error} />} | ||
| 57 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 58 | + </div> | ||
| 59 | + ) | ||
| 60 | +} |
web/src/pages/SalesOrderDetailPage.tsx
0 → 100644
| 1 | +// Sales-order detail screen. | ||
| 2 | +// | ||
| 3 | +// **The headline demo flow.** Confirm a DRAFT, ship a CONFIRMED, | ||
| 4 | +// or cancel either. Each action updates the order in place, | ||
| 5 | +// reloads the journal entry list to show the AR row appearing | ||
| 6 | +// (POSTED → SETTLED), and reloads stock movements to show the | ||
| 7 | +// SALES_SHIPMENT ledger entry appearing. | ||
| 8 | +// | ||
| 9 | +// **Why ship asks for a location.** The framework requires every | ||
| 10 | +// shipment to name the warehouse the goods came from — the | ||
| 11 | +// inventory ledger tags the row with that location and the audit | ||
| 12 | +// trail must always answer "which warehouse shipped this". The UI | ||
| 13 | +// proposes the first WAREHOUSE-typed location it finds; an | ||
| 14 | +// operator can pick another from the dropdown. | ||
| 15 | + | ||
| 16 | +import { useCallback, useEffect, useState } from 'react' | ||
| 17 | +import { Link, useNavigate, useParams } from 'react-router-dom' | ||
| 18 | +import { finance, inventory, salesOrders } from '@/api/client' | ||
| 19 | +import type { JournalEntry, Location, SalesOrder, StockMovement } from '@/types/api' | ||
| 20 | +import { PageHeader } from '@/components/PageHeader' | ||
| 21 | +import { Loading } from '@/components/Loading' | ||
| 22 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 23 | +import { StatusBadge } from '@/components/StatusBadge' | ||
| 24 | + | ||
| 25 | +export function SalesOrderDetailPage() { | ||
| 26 | + const { id = '' } = useParams<{ id: string }>() | ||
| 27 | + const navigate = useNavigate() | ||
| 28 | + const [order, setOrder] = useState<SalesOrder | null>(null) | ||
| 29 | + const [locations, setLocations] = useState<Location[]>([]) | ||
| 30 | + const [shippingLocation, setShippingLocation] = useState<string>('') | ||
| 31 | + const [movements, setMovements] = useState<StockMovement[]>([]) | ||
| 32 | + const [journalEntries, setJournalEntries] = useState<JournalEntry[]>([]) | ||
| 33 | + const [loading, setLoading] = useState(true) | ||
| 34 | + const [acting, setActing] = useState(false) | ||
| 35 | + const [error, setError] = useState<Error | null>(null) | ||
| 36 | + const [actionMessage, setActionMessage] = useState<string | null>(null) | ||
| 37 | + | ||
| 38 | + const reloadSideEffects = useCallback(async (orderCode: string) => { | ||
| 39 | + const [ms, js] = await Promise.all([ | ||
| 40 | + inventory.listMovements(), | ||
| 41 | + finance.listJournalEntries(), | ||
| 42 | + ]) | ||
| 43 | + setMovements(ms.filter((m) => m.reference?.includes(orderCode) ?? false)) | ||
| 44 | + setJournalEntries(js.filter((j) => j.orderCode === orderCode)) | ||
| 45 | + }, []) | ||
| 46 | + | ||
| 47 | + useEffect(() => { | ||
| 48 | + let active = true | ||
| 49 | + setLoading(true) | ||
| 50 | + Promise.all([salesOrders.get(id), inventory.listLocations()]) | ||
| 51 | + .then(async ([o, locs]: [SalesOrder, Location[]]) => { | ||
| 52 | + if (!active) return | ||
| 53 | + setOrder(o) | ||
| 54 | + setLocations(locs.filter((l) => l.active)) | ||
| 55 | + const firstWarehouse = locs.find((l) => l.active && l.type === 'WAREHOUSE') | ||
| 56 | + setShippingLocation(firstWarehouse?.code ?? locs[0]?.code ?? '') | ||
| 57 | + await reloadSideEffects(o.code) | ||
| 58 | + }) | ||
| 59 | + .catch((e: unknown) => { | ||
| 60 | + if (active) setError(e instanceof Error ? e : new Error(String(e))) | ||
| 61 | + }) | ||
| 62 | + .finally(() => active && setLoading(false)) | ||
| 63 | + return () => { | ||
| 64 | + active = false | ||
| 65 | + } | ||
| 66 | + }, [id, reloadSideEffects]) | ||
| 67 | + | ||
| 68 | + if (loading) return <Loading /> | ||
| 69 | + if (error) return <ErrorBox error={error} /> | ||
| 70 | + if (!order) return <ErrorBox error="Sales order not found" /> | ||
| 71 | + | ||
| 72 | + const onConfirm = async () => { | ||
| 73 | + setActing(true) | ||
| 74 | + setError(null) | ||
| 75 | + setActionMessage(null) | ||
| 76 | + try { | ||
| 77 | + const updated = await salesOrders.confirm(order.id) | ||
| 78 | + setOrder(updated) | ||
| 79 | + await reloadSideEffects(updated.code) | ||
| 80 | + setActionMessage('Confirmed. pbc-finance has posted an AR journal entry.') | ||
| 81 | + } catch (e: unknown) { | ||
| 82 | + setError(e instanceof Error ? e : new Error(String(e))) | ||
| 83 | + } finally { | ||
| 84 | + setActing(false) | ||
| 85 | + } | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + const onShip = async () => { | ||
| 89 | + if (!shippingLocation) { | ||
| 90 | + setError(new Error('Pick a shipping location first.')) | ||
| 91 | + return | ||
| 92 | + } | ||
| 93 | + setActing(true) | ||
| 94 | + setError(null) | ||
| 95 | + setActionMessage(null) | ||
| 96 | + try { | ||
| 97 | + const updated = await salesOrders.ship(order.id, shippingLocation) | ||
| 98 | + setOrder(updated) | ||
| 99 | + await reloadSideEffects(updated.code) | ||
| 100 | + setActionMessage( | ||
| 101 | + `Shipped from ${shippingLocation}. Stock debited, journal entry settled.`, | ||
| 102 | + ) | ||
| 103 | + } catch (e: unknown) { | ||
| 104 | + setError(e instanceof Error ? e : new Error(String(e))) | ||
| 105 | + } finally { | ||
| 106 | + setActing(false) | ||
| 107 | + } | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + const onCancel = async () => { | ||
| 111 | + setActing(true) | ||
| 112 | + setError(null) | ||
| 113 | + setActionMessage(null) | ||
| 114 | + try { | ||
| 115 | + const updated = await salesOrders.cancel(order.id) | ||
| 116 | + setOrder(updated) | ||
| 117 | + await reloadSideEffects(updated.code) | ||
| 118 | + setActionMessage('Cancelled. pbc-finance has reversed any open journal entry.') | ||
| 119 | + } catch (e: unknown) { | ||
| 120 | + setError(e instanceof Error ? e : new Error(String(e))) | ||
| 121 | + } finally { | ||
| 122 | + setActing(false) | ||
| 123 | + } | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + const canConfirm = order.status === 'DRAFT' | ||
| 127 | + const canShip = order.status === 'CONFIRMED' | ||
| 128 | + const canCancel = order.status === 'DRAFT' || order.status === 'CONFIRMED' | ||
| 129 | + | ||
| 130 | + return ( | ||
| 131 | + <div> | ||
| 132 | + <PageHeader | ||
| 133 | + title={`Sales Order ${order.code}`} | ||
| 134 | + subtitle={`Customer ${order.partnerCode} · ${order.orderDate} · ${order.currencyCode}`} | ||
| 135 | + actions={ | ||
| 136 | + <button className="btn-secondary" onClick={() => navigate('/sales-orders')}> | ||
| 137 | + ← Back | ||
| 138 | + </button> | ||
| 139 | + } | ||
| 140 | + /> | ||
| 141 | + | ||
| 142 | + <div className="card mb-6 p-5"> | ||
| 143 | + <div className="flex flex-wrap items-center justify-between gap-4"> | ||
| 144 | + <div className="flex items-center gap-3"> | ||
| 145 | + <span className="text-sm text-slate-500">Status:</span> | ||
| 146 | + <StatusBadge status={order.status} /> | ||
| 147 | + </div> | ||
| 148 | + <div className="text-right"> | ||
| 149 | + <div className="text-xs uppercase text-slate-400">Total</div> | ||
| 150 | + <div className="font-mono text-xl font-semibold"> | ||
| 151 | + {Number(order.totalAmount).toLocaleString(undefined, { | ||
| 152 | + minimumFractionDigits: 2, | ||
| 153 | + maximumFractionDigits: 2, | ||
| 154 | + })}{' '} | ||
| 155 | + {order.currencyCode} | ||
| 156 | + </div> | ||
| 157 | + </div> | ||
| 158 | + </div> | ||
| 159 | + {actionMessage && ( | ||
| 160 | + <div className="mt-4 rounded-md border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm text-emerald-800"> | ||
| 161 | + {actionMessage} | ||
| 162 | + </div> | ||
| 163 | + )} | ||
| 164 | + </div> | ||
| 165 | + | ||
| 166 | + <div className="card mb-6 p-5"> | ||
| 167 | + <h2 className="mb-3 text-base font-semibold text-slate-800">Actions</h2> | ||
| 168 | + <div className="flex flex-wrap items-center gap-3"> | ||
| 169 | + <button className="btn-primary" disabled={!canConfirm || acting} onClick={onConfirm}> | ||
| 170 | + Confirm | ||
| 171 | + </button> | ||
| 172 | + <div className="flex items-center gap-2"> | ||
| 173 | + <select | ||
| 174 | + className="rounded-md border border-slate-300 px-2 py-1 text-sm shadow-sm focus:border-brand-500 focus:ring-brand-500" | ||
| 175 | + value={shippingLocation} | ||
| 176 | + onChange={(e) => setShippingLocation(e.target.value)} | ||
| 177 | + disabled={!canShip || acting} | ||
| 178 | + > | ||
| 179 | + {locations.map((l) => ( | ||
| 180 | + <option key={l.id} value={l.code}> | ||
| 181 | + {l.code} — {l.name} | ||
| 182 | + </option> | ||
| 183 | + ))} | ||
| 184 | + </select> | ||
| 185 | + <button className="btn-primary" disabled={!canShip || acting} onClick={onShip}> | ||
| 186 | + Ship | ||
| 187 | + </button> | ||
| 188 | + </div> | ||
| 189 | + <button className="btn-danger" disabled={!canCancel || acting} onClick={onCancel}> | ||
| 190 | + Cancel | ||
| 191 | + </button> | ||
| 192 | + </div> | ||
| 193 | + </div> | ||
| 194 | + | ||
| 195 | + <div className="card mb-6"> | ||
| 196 | + <div className="border-b border-slate-200 px-5 py-3"> | ||
| 197 | + <h2 className="text-base font-semibold text-slate-800">Lines</h2> | ||
| 198 | + </div> | ||
| 199 | + <table className="table-base"> | ||
| 200 | + <thead className="bg-slate-50"> | ||
| 201 | + <tr> | ||
| 202 | + <th>#</th> | ||
| 203 | + <th>Item</th> | ||
| 204 | + <th>Qty</th> | ||
| 205 | + <th>Unit price</th> | ||
| 206 | + <th>Line total</th> | ||
| 207 | + </tr> | ||
| 208 | + </thead> | ||
| 209 | + <tbody className="divide-y divide-slate-100"> | ||
| 210 | + {order.lines.map((l) => ( | ||
| 211 | + <tr key={l.id}> | ||
| 212 | + <td>{l.lineNo}</td> | ||
| 213 | + <td className="font-mono">{l.itemCode}</td> | ||
| 214 | + <td className="font-mono tabular-nums">{String(l.quantity)}</td> | ||
| 215 | + <td className="font-mono tabular-nums"> | ||
| 216 | + {Number(l.unitPrice).toLocaleString(undefined, { | ||
| 217 | + minimumFractionDigits: 2, | ||
| 218 | + })}{' '} | ||
| 219 | + {l.currencyCode} | ||
| 220 | + </td> | ||
| 221 | + <td className="font-mono tabular-nums"> | ||
| 222 | + {Number(l.lineTotal).toLocaleString(undefined, { | ||
| 223 | + minimumFractionDigits: 2, | ||
| 224 | + })} | ||
| 225 | + </td> | ||
| 226 | + </tr> | ||
| 227 | + ))} | ||
| 228 | + </tbody> | ||
| 229 | + </table> | ||
| 230 | + </div> | ||
| 231 | + | ||
| 232 | + <div className="grid gap-6 md:grid-cols-2"> | ||
| 233 | + <div className="card"> | ||
| 234 | + <div className="border-b border-slate-200 px-5 py-3"> | ||
| 235 | + <h2 className="text-base font-semibold text-slate-800">Inventory movements</h2> | ||
| 236 | + </div> | ||
| 237 | + {movements.length === 0 ? ( | ||
| 238 | + <div className="p-5 text-sm text-slate-400">No movements yet.</div> | ||
| 239 | + ) : ( | ||
| 240 | + <table className="table-base"> | ||
| 241 | + <thead className="bg-slate-50"> | ||
| 242 | + <tr> | ||
| 243 | + <th>Item</th> | ||
| 244 | + <th>Δ</th> | ||
| 245 | + <th>Reason</th> | ||
| 246 | + <th>Reference</th> | ||
| 247 | + </tr> | ||
| 248 | + </thead> | ||
| 249 | + <tbody className="divide-y divide-slate-100"> | ||
| 250 | + {movements.map((m) => ( | ||
| 251 | + <tr key={m.id}> | ||
| 252 | + <td className="font-mono">{m.itemCode}</td> | ||
| 253 | + <td className="font-mono tabular-nums text-rose-600">{String(m.delta)}</td> | ||
| 254 | + <td>{m.reason}</td> | ||
| 255 | + <td className="font-mono text-xs text-slate-500">{m.reference ?? '—'}</td> | ||
| 256 | + </tr> | ||
| 257 | + ))} | ||
| 258 | + </tbody> | ||
| 259 | + </table> | ||
| 260 | + )} | ||
| 261 | + </div> | ||
| 262 | + <div className="card"> | ||
| 263 | + <div className="border-b border-slate-200 px-5 py-3"> | ||
| 264 | + <h2 className="text-base font-semibold text-slate-800">Journal entries</h2> | ||
| 265 | + </div> | ||
| 266 | + {journalEntries.length === 0 ? ( | ||
| 267 | + <div className="p-5 text-sm text-slate-400">No entries yet.</div> | ||
| 268 | + ) : ( | ||
| 269 | + <table className="table-base"> | ||
| 270 | + <thead className="bg-slate-50"> | ||
| 271 | + <tr> | ||
| 272 | + <th>Code</th> | ||
| 273 | + <th>Type</th> | ||
| 274 | + <th>Status</th> | ||
| 275 | + <th>Amount</th> | ||
| 276 | + </tr> | ||
| 277 | + </thead> | ||
| 278 | + <tbody className="divide-y divide-slate-100"> | ||
| 279 | + {journalEntries.map((j) => ( | ||
| 280 | + <tr key={j.id}> | ||
| 281 | + <td className="font-mono text-xs">{j.code}</td> | ||
| 282 | + <td>{j.type}</td> | ||
| 283 | + <td> | ||
| 284 | + <StatusBadge status={j.status} /> | ||
| 285 | + </td> | ||
| 286 | + <td className="font-mono tabular-nums"> | ||
| 287 | + {Number(j.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })}{' '} | ||
| 288 | + {j.currencyCode} | ||
| 289 | + </td> | ||
| 290 | + </tr> | ||
| 291 | + ))} | ||
| 292 | + </tbody> | ||
| 293 | + </table> | ||
| 294 | + )} | ||
| 295 | + <div className="border-t border-slate-200 px-5 py-3 text-xs text-slate-400"> | ||
| 296 | + <Link to="/journal-entries" className="hover:underline">View all journal entries →</Link> | ||
| 297 | + </div> | ||
| 298 | + </div> | ||
| 299 | + </div> | ||
| 300 | + </div> | ||
| 301 | + ) | ||
| 302 | +} |
web/src/pages/SalesOrdersPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { Link } from 'react-router-dom' | ||
| 3 | +import { salesOrders } from '@/api/client' | ||
| 4 | +import type { SalesOrder } from '@/types/api' | ||
| 5 | +import { PageHeader } from '@/components/PageHeader' | ||
| 6 | +import { Loading } from '@/components/Loading' | ||
| 7 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 8 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 9 | +import { StatusBadge } from '@/components/StatusBadge' | ||
| 10 | + | ||
| 11 | +export function SalesOrdersPage() { | ||
| 12 | + const [rows, setRows] = useState<SalesOrder[]>([]) | ||
| 13 | + const [error, setError] = useState<Error | null>(null) | ||
| 14 | + const [loading, setLoading] = useState(true) | ||
| 15 | + | ||
| 16 | + useEffect(() => { | ||
| 17 | + salesOrders | ||
| 18 | + .list() | ||
| 19 | + .then((rs) => setRows([...rs].sort((a, b) => a.code.localeCompare(b.code)))) | ||
| 20 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 21 | + .finally(() => setLoading(false)) | ||
| 22 | + }, []) | ||
| 23 | + | ||
| 24 | + const columns: Column<SalesOrder>[] = [ | ||
| 25 | + { | ||
| 26 | + header: 'Code', | ||
| 27 | + key: 'code', | ||
| 28 | + render: (r) => ( | ||
| 29 | + <Link to={`/sales-orders/${r.id}`} className="font-mono text-brand-600 hover:underline"> | ||
| 30 | + {r.code} | ||
| 31 | + </Link> | ||
| 32 | + ), | ||
| 33 | + }, | ||
| 34 | + { header: 'Customer', key: 'partnerCode', render: (r) => <span className="font-mono">{r.partnerCode}</span> }, | ||
| 35 | + { header: 'Date', key: 'orderDate' }, | ||
| 36 | + { header: 'Status', key: 'status', render: (r) => <StatusBadge status={r.status} /> }, | ||
| 37 | + { | ||
| 38 | + header: 'Total', | ||
| 39 | + key: 'totalAmount', | ||
| 40 | + render: (r) => ( | ||
| 41 | + <span className="font-mono tabular-nums"> | ||
| 42 | + {Number(r.totalAmount).toLocaleString(undefined, { | ||
| 43 | + minimumFractionDigits: 2, | ||
| 44 | + maximumFractionDigits: 2, | ||
| 45 | + })}{' '} | ||
| 46 | + {r.currencyCode} | ||
| 47 | + </span> | ||
| 48 | + ), | ||
| 49 | + }, | ||
| 50 | + { | ||
| 51 | + header: 'Lines', | ||
| 52 | + key: 'lines', | ||
| 53 | + render: (r) => <span className="text-slate-500">{r.lines.length}</span>, | ||
| 54 | + }, | ||
| 55 | + ] | ||
| 56 | + | ||
| 57 | + return ( | ||
| 58 | + <div> | ||
| 59 | + <PageHeader title="Sales Orders" subtitle="Customer-facing orders. Click a code to drill in." /> | ||
| 60 | + {loading && <Loading />} | ||
| 61 | + {error && <ErrorBox error={error} />} | ||
| 62 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 63 | + </div> | ||
| 64 | + ) | ||
| 65 | +} |
web/src/pages/ShopFloorPage.tsx
0 → 100644
| 1 | +// Shop-floor dashboard. | ||
| 2 | +// | ||
| 3 | +// Polls /api/v1/production/work-orders/shop-floor every 5s and | ||
| 4 | +// renders one card per IN_PROGRESS work order with its current | ||
| 5 | +// operation, planned vs actual minutes, and operations completed. | ||
| 6 | +// Designed to be projected on a wall-mounted screen — the cards | ||
| 7 | +// are large, the typography is high-contrast, and the only state | ||
| 8 | +// is "what's running right now". | ||
| 9 | + | ||
| 10 | +import { useEffect, useState } from 'react' | ||
| 11 | +import { Link } from 'react-router-dom' | ||
| 12 | +import { production } from '@/api/client' | ||
| 13 | +import type { ShopFloorEntry } from '@/types/api' | ||
| 14 | +import { PageHeader } from '@/components/PageHeader' | ||
| 15 | +import { Loading } from '@/components/Loading' | ||
| 16 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 17 | +import { StatusBadge } from '@/components/StatusBadge' | ||
| 18 | + | ||
| 19 | +const POLL_MS = 5000 | ||
| 20 | + | ||
| 21 | +export function ShopFloorPage() { | ||
| 22 | + const [rows, setRows] = useState<ShopFloorEntry[]>([]) | ||
| 23 | + const [error, setError] = useState<Error | null>(null) | ||
| 24 | + const [loading, setLoading] = useState(true) | ||
| 25 | + const [updatedAt, setUpdatedAt] = useState<Date | null>(null) | ||
| 26 | + | ||
| 27 | + useEffect(() => { | ||
| 28 | + let active = true | ||
| 29 | + let timer: number | null = null | ||
| 30 | + | ||
| 31 | + const tick = async () => { | ||
| 32 | + try { | ||
| 33 | + const data = await production.shopFloor() | ||
| 34 | + if (!active) return | ||
| 35 | + setRows(data) | ||
| 36 | + setUpdatedAt(new Date()) | ||
| 37 | + setError(null) | ||
| 38 | + } catch (e: unknown) { | ||
| 39 | + if (active) setError(e instanceof Error ? e : new Error(String(e))) | ||
| 40 | + } finally { | ||
| 41 | + if (active) setLoading(false) | ||
| 42 | + } | ||
| 43 | + if (active) timer = window.setTimeout(tick, POLL_MS) | ||
| 44 | + } | ||
| 45 | + tick() | ||
| 46 | + return () => { | ||
| 47 | + active = false | ||
| 48 | + if (timer !== null) window.clearTimeout(timer) | ||
| 49 | + } | ||
| 50 | + }, []) | ||
| 51 | + | ||
| 52 | + return ( | ||
| 53 | + <div> | ||
| 54 | + <PageHeader | ||
| 55 | + title="Shop Floor" | ||
| 56 | + subtitle={`Live view of every work order in progress · refreshes every ${POLL_MS / 1000}s${ | ||
| 57 | + updatedAt ? ' · last update ' + updatedAt.toLocaleTimeString() : '' | ||
| 58 | + }`} | ||
| 59 | + /> | ||
| 60 | + {loading && <Loading />} | ||
| 61 | + {error && <ErrorBox error={error} />} | ||
| 62 | + {!loading && rows.length === 0 && ( | ||
| 63 | + <div className="card p-8 text-center text-sm text-slate-400"> | ||
| 64 | + No work orders are in progress right now. | ||
| 65 | + </div> | ||
| 66 | + )} | ||
| 67 | + <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> | ||
| 68 | + {rows.map((r) => { | ||
| 69 | + const std = Number(r.totalStandardMinutes) | ||
| 70 | + const act = Number(r.totalActualMinutes) | ||
| 71 | + const pct = std > 0 ? Math.min(100, Math.round((act / std) * 100)) : 0 | ||
| 72 | + return ( | ||
| 73 | + <Link | ||
| 74 | + key={r.workOrderId} | ||
| 75 | + to={`/work-orders/${r.workOrderId}`} | ||
| 76 | + className="card p-5 transition hover:shadow-md" | ||
| 77 | + > | ||
| 78 | + <div className="mb-2 flex items-center justify-between"> | ||
| 79 | + <span className="font-mono text-lg font-semibold text-brand-600"> | ||
| 80 | + {r.workOrderCode} | ||
| 81 | + </span> | ||
| 82 | + <span className="text-xs text-slate-500"> | ||
| 83 | + {r.operationsCompleted} / {r.operationsTotal} ops | ||
| 84 | + </span> | ||
| 85 | + </div> | ||
| 86 | + <div className="mb-3 text-xs text-slate-500"> | ||
| 87 | + Output: <span className="font-mono">{r.outputItemCode}</span> ×{' '} | ||
| 88 | + {String(r.outputQuantity)} | ||
| 89 | + </div> | ||
| 90 | + <div className="mb-3"> | ||
| 91 | + <div className="text-xs text-slate-400">Current operation</div> | ||
| 92 | + {r.currentOperationCode ? ( | ||
| 93 | + <div className="mt-1 flex items-center gap-2"> | ||
| 94 | + <span className="font-mono font-medium">{r.currentOperationCode}</span> | ||
| 95 | + <span className="text-xs text-slate-500">@ {r.currentWorkCenter}</span> | ||
| 96 | + {r.currentOperationStatus && <StatusBadge status={r.currentOperationStatus} />} | ||
| 97 | + </div> | ||
| 98 | + ) : ( | ||
| 99 | + <div className="mt-1 text-sm text-slate-400">No routing</div> | ||
| 100 | + )} | ||
| 101 | + </div> | ||
| 102 | + <div className="mt-2"> | ||
| 103 | + <div className="mb-1 flex items-center justify-between text-xs text-slate-500"> | ||
| 104 | + <span>{act.toFixed(0)} actual min</span> | ||
| 105 | + <span>{std.toFixed(0)} std min</span> | ||
| 106 | + </div> | ||
| 107 | + <div className="h-2 overflow-hidden rounded-full bg-slate-100"> | ||
| 108 | + <div | ||
| 109 | + className="h-full bg-brand-500 transition-all" | ||
| 110 | + style={{ width: `${pct}%` }} | ||
| 111 | + /> | ||
| 112 | + </div> | ||
| 113 | + </div> | ||
| 114 | + </Link> | ||
| 115 | + ) | ||
| 116 | + })} | ||
| 117 | + </div> | ||
| 118 | + </div> | ||
| 119 | + ) | ||
| 120 | +} |
web/src/pages/UomsPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { catalog } from '@/api/client' | ||
| 3 | +import type { Uom } from '@/types/api' | ||
| 4 | +import { PageHeader } from '@/components/PageHeader' | ||
| 5 | +import { Loading } from '@/components/Loading' | ||
| 6 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 7 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 8 | + | ||
| 9 | +export function UomsPage() { | ||
| 10 | + const [rows, setRows] = useState<Uom[]>([]) | ||
| 11 | + const [error, setError] = useState<Error | null>(null) | ||
| 12 | + const [loading, setLoading] = useState(true) | ||
| 13 | + | ||
| 14 | + useEffect(() => { | ||
| 15 | + catalog | ||
| 16 | + .listUoms() | ||
| 17 | + .then(setRows) | ||
| 18 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 19 | + .finally(() => setLoading(false)) | ||
| 20 | + }, []) | ||
| 21 | + | ||
| 22 | + const columns: Column<Uom>[] = [ | ||
| 23 | + { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> }, | ||
| 24 | + { header: 'Name', key: 'name' }, | ||
| 25 | + { header: 'Dimension', key: 'dimension' }, | ||
| 26 | + ] | ||
| 27 | + | ||
| 28 | + return ( | ||
| 29 | + <div> | ||
| 30 | + <PageHeader | ||
| 31 | + title="Units of Measure" | ||
| 32 | + subtitle="Seeded set of UoMs the catalog and inventory PBCs use to quantify items." | ||
| 33 | + /> | ||
| 34 | + {loading && <Loading />} | ||
| 35 | + {error && <ErrorBox error={error} />} | ||
| 36 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 37 | + </div> | ||
| 38 | + ) | ||
| 39 | +} |
web/src/pages/WorkOrderDetailPage.tsx
0 → 100644
| 1 | +// Work-order detail screen — read-only header + start/complete | ||
| 2 | +// action verbs that drive the v2 state machine. The shop-floor | ||
| 3 | +// dashboard at /shop-floor handles the per-operation walk for v3 | ||
| 4 | +// routing-equipped orders. v1 SPA keeps this screen simple: start a | ||
| 5 | +// DRAFT, complete an IN_PROGRESS into a warehouse. | ||
| 6 | + | ||
| 7 | +import { useCallback, useEffect, useState } from 'react' | ||
| 8 | +import { useNavigate, useParams } from 'react-router-dom' | ||
| 9 | +import { inventory, production } from '@/api/client' | ||
| 10 | +import type { Location, WorkOrder } from '@/types/api' | ||
| 11 | +import { PageHeader } from '@/components/PageHeader' | ||
| 12 | +import { Loading } from '@/components/Loading' | ||
| 13 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 14 | +import { StatusBadge } from '@/components/StatusBadge' | ||
| 15 | + | ||
| 16 | +export function WorkOrderDetailPage() { | ||
| 17 | + const { id = '' } = useParams<{ id: string }>() | ||
| 18 | + const navigate = useNavigate() | ||
| 19 | + const [order, setOrder] = useState<WorkOrder | null>(null) | ||
| 20 | + const [locations, setLocations] = useState<Location[]>([]) | ||
| 21 | + const [outputLocation, setOutputLocation] = useState<string>('') | ||
| 22 | + const [loading, setLoading] = useState(true) | ||
| 23 | + const [acting, setActing] = useState(false) | ||
| 24 | + const [error, setError] = useState<Error | null>(null) | ||
| 25 | + const [actionMessage, setActionMessage] = useState<string | null>(null) | ||
| 26 | + | ||
| 27 | + const refresh = useCallback(async () => { | ||
| 28 | + const o = await production.getWorkOrder(id) | ||
| 29 | + setOrder(o) | ||
| 30 | + }, [id]) | ||
| 31 | + | ||
| 32 | + useEffect(() => { | ||
| 33 | + let active = true | ||
| 34 | + setLoading(true) | ||
| 35 | + Promise.all([production.getWorkOrder(id), inventory.listLocations()]) | ||
| 36 | + .then(([o, locs]: [WorkOrder, Location[]]) => { | ||
| 37 | + if (!active) return | ||
| 38 | + setOrder(o) | ||
| 39 | + setLocations(locs.filter((l) => l.active)) | ||
| 40 | + const firstWarehouse = locs.find((l) => l.active && l.type === 'WAREHOUSE') | ||
| 41 | + setOutputLocation(firstWarehouse?.code ?? locs[0]?.code ?? '') | ||
| 42 | + }) | ||
| 43 | + .catch((e: unknown) => { | ||
| 44 | + if (active) setError(e instanceof Error ? e : new Error(String(e))) | ||
| 45 | + }) | ||
| 46 | + .finally(() => active && setLoading(false)) | ||
| 47 | + return () => { | ||
| 48 | + active = false | ||
| 49 | + } | ||
| 50 | + }, [id]) | ||
| 51 | + | ||
| 52 | + if (loading) return <Loading /> | ||
| 53 | + if (error) return <ErrorBox error={error} /> | ||
| 54 | + if (!order) return <ErrorBox error="Work order not found" /> | ||
| 55 | + | ||
| 56 | + const onStart = async () => { | ||
| 57 | + setActing(true) | ||
| 58 | + setError(null) | ||
| 59 | + setActionMessage(null) | ||
| 60 | + try { | ||
| 61 | + await production.startWorkOrder(order.id) | ||
| 62 | + await refresh() | ||
| 63 | + setActionMessage('Started. Operations can now be walked from the Shop Floor screen.') | ||
| 64 | + } catch (e: unknown) { | ||
| 65 | + setError(e instanceof Error ? e : new Error(String(e))) | ||
| 66 | + } finally { | ||
| 67 | + setActing(false) | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + const onComplete = async () => { | ||
| 72 | + if (!outputLocation) { | ||
| 73 | + setError(new Error('Pick an output location first.')) | ||
| 74 | + return | ||
| 75 | + } | ||
| 76 | + setActing(true) | ||
| 77 | + setError(null) | ||
| 78 | + setActionMessage(null) | ||
| 79 | + try { | ||
| 80 | + await production.completeWorkOrder(order.id, outputLocation) | ||
| 81 | + await refresh() | ||
| 82 | + setActionMessage( | ||
| 83 | + `Completed. Materials issued, finished goods credited to ${outputLocation}.`, | ||
| 84 | + ) | ||
| 85 | + } catch (e: unknown) { | ||
| 86 | + setError(e instanceof Error ? e : new Error(String(e))) | ||
| 87 | + } finally { | ||
| 88 | + setActing(false) | ||
| 89 | + } | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + const canStart = order.status === 'DRAFT' | ||
| 93 | + const canComplete = order.status === 'IN_PROGRESS' | ||
| 94 | + | ||
| 95 | + return ( | ||
| 96 | + <div> | ||
| 97 | + <PageHeader | ||
| 98 | + title={`Work Order ${order.code}`} | ||
| 99 | + subtitle={`Output: ${order.outputItemCode} × ${order.outputQuantity}${ | ||
| 100 | + order.sourceSalesOrderCode ? ' · from SO ' + order.sourceSalesOrderCode : '' | ||
| 101 | + }`} | ||
| 102 | + actions={ | ||
| 103 | + <button className="btn-secondary" onClick={() => navigate('/work-orders')}> | ||
| 104 | + ← Back | ||
| 105 | + </button> | ||
| 106 | + } | ||
| 107 | + /> | ||
| 108 | + | ||
| 109 | + <div className="card mb-6 p-5"> | ||
| 110 | + <div className="flex items-center gap-3"> | ||
| 111 | + <span className="text-sm text-slate-500">Status:</span> | ||
| 112 | + <StatusBadge status={order.status} /> | ||
| 113 | + </div> | ||
| 114 | + {actionMessage && ( | ||
| 115 | + <div className="mt-4 rounded-md border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm text-emerald-800"> | ||
| 116 | + {actionMessage} | ||
| 117 | + </div> | ||
| 118 | + )} | ||
| 119 | + </div> | ||
| 120 | + | ||
| 121 | + <div className="card mb-6 p-5"> | ||
| 122 | + <h2 className="mb-3 text-base font-semibold text-slate-800">Actions</h2> | ||
| 123 | + <div className="flex flex-wrap items-center gap-3"> | ||
| 124 | + <button className="btn-primary" disabled={!canStart || acting} onClick={onStart}> | ||
| 125 | + Start | ||
| 126 | + </button> | ||
| 127 | + <div className="flex items-center gap-2"> | ||
| 128 | + <select | ||
| 129 | + className="rounded-md border border-slate-300 px-2 py-1 text-sm shadow-sm focus:border-brand-500 focus:ring-brand-500" | ||
| 130 | + value={outputLocation} | ||
| 131 | + onChange={(e) => setOutputLocation(e.target.value)} | ||
| 132 | + disabled={!canComplete || acting} | ||
| 133 | + > | ||
| 134 | + {locations.map((l) => ( | ||
| 135 | + <option key={l.id} value={l.code}> | ||
| 136 | + {l.code} — {l.name} | ||
| 137 | + </option> | ||
| 138 | + ))} | ||
| 139 | + </select> | ||
| 140 | + <button className="btn-primary" disabled={!canComplete || acting} onClick={onComplete}> | ||
| 141 | + Complete | ||
| 142 | + </button> | ||
| 143 | + </div> | ||
| 144 | + </div> | ||
| 145 | + </div> | ||
| 146 | + | ||
| 147 | + <div className="grid gap-6 md:grid-cols-2"> | ||
| 148 | + <div className="card"> | ||
| 149 | + <div className="border-b border-slate-200 px-5 py-3"> | ||
| 150 | + <h2 className="text-base font-semibold text-slate-800">BOM inputs</h2> | ||
| 151 | + </div> | ||
| 152 | + {order.inputs.length === 0 ? ( | ||
| 153 | + <div className="p-5 text-sm text-slate-400">No BOM lines.</div> | ||
| 154 | + ) : ( | ||
| 155 | + <table className="table-base"> | ||
| 156 | + <thead className="bg-slate-50"> | ||
| 157 | + <tr> | ||
| 158 | + <th>#</th> | ||
| 159 | + <th>Item</th> | ||
| 160 | + <th>Qty / unit</th> | ||
| 161 | + <th>Source loc</th> | ||
| 162 | + </tr> | ||
| 163 | + </thead> | ||
| 164 | + <tbody className="divide-y divide-slate-100"> | ||
| 165 | + {order.inputs.map((i) => ( | ||
| 166 | + <tr key={i.id}> | ||
| 167 | + <td>{i.lineNo}</td> | ||
| 168 | + <td className="font-mono">{i.itemCode}</td> | ||
| 169 | + <td className="font-mono tabular-nums">{String(i.quantityPerUnit)}</td> | ||
| 170 | + <td className="font-mono">{i.sourceLocationCode}</td> | ||
| 171 | + </tr> | ||
| 172 | + ))} | ||
| 173 | + </tbody> | ||
| 174 | + </table> | ||
| 175 | + )} | ||
| 176 | + </div> | ||
| 177 | + <div className="card"> | ||
| 178 | + <div className="border-b border-slate-200 px-5 py-3"> | ||
| 179 | + <h2 className="text-base font-semibold text-slate-800">Routing operations</h2> | ||
| 180 | + </div> | ||
| 181 | + {order.operations.length === 0 ? ( | ||
| 182 | + <div className="p-5 text-sm text-slate-400">No routing.</div> | ||
| 183 | + ) : ( | ||
| 184 | + <table className="table-base"> | ||
| 185 | + <thead className="bg-slate-50"> | ||
| 186 | + <tr> | ||
| 187 | + <th>#</th> | ||
| 188 | + <th>Operation</th> | ||
| 189 | + <th>Work center</th> | ||
| 190 | + <th>Std min</th> | ||
| 191 | + <th>Status</th> | ||
| 192 | + </tr> | ||
| 193 | + </thead> | ||
| 194 | + <tbody className="divide-y divide-slate-100"> | ||
| 195 | + {order.operations.map((o) => ( | ||
| 196 | + <tr key={o.id}> | ||
| 197 | + <td>{o.lineNo}</td> | ||
| 198 | + <td className="font-mono">{o.operationCode}</td> | ||
| 199 | + <td className="font-mono">{o.workCenter}</td> | ||
| 200 | + <td className="font-mono tabular-nums">{String(o.standardMinutes)}</td> | ||
| 201 | + <td> | ||
| 202 | + <StatusBadge status={o.status} /> | ||
| 203 | + </td> | ||
| 204 | + </tr> | ||
| 205 | + ))} | ||
| 206 | + </tbody> | ||
| 207 | + </table> | ||
| 208 | + )} | ||
| 209 | + </div> | ||
| 210 | + </div> | ||
| 211 | + </div> | ||
| 212 | + ) | ||
| 213 | +} |
web/src/pages/WorkOrdersPage.tsx
0 → 100644
| 1 | +import { useEffect, useState } from 'react' | ||
| 2 | +import { Link } from 'react-router-dom' | ||
| 3 | +import { production } from '@/api/client' | ||
| 4 | +import type { WorkOrder } from '@/types/api' | ||
| 5 | +import { PageHeader } from '@/components/PageHeader' | ||
| 6 | +import { Loading } from '@/components/Loading' | ||
| 7 | +import { ErrorBox } from '@/components/ErrorBox' | ||
| 8 | +import { DataTable, type Column } from '@/components/DataTable' | ||
| 9 | +import { StatusBadge } from '@/components/StatusBadge' | ||
| 10 | + | ||
| 11 | +export function WorkOrdersPage() { | ||
| 12 | + const [rows, setRows] = useState<WorkOrder[]>([]) | ||
| 13 | + const [error, setError] = useState<Error | null>(null) | ||
| 14 | + const [loading, setLoading] = useState(true) | ||
| 15 | + | ||
| 16 | + useEffect(() => { | ||
| 17 | + production | ||
| 18 | + .listWorkOrders() | ||
| 19 | + .then((rs) => setRows([...rs].sort((a, b) => a.code.localeCompare(b.code)))) | ||
| 20 | + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | ||
| 21 | + .finally(() => setLoading(false)) | ||
| 22 | + }, []) | ||
| 23 | + | ||
| 24 | + const columns: Column<WorkOrder>[] = [ | ||
| 25 | + { | ||
| 26 | + header: 'Code', | ||
| 27 | + key: 'code', | ||
| 28 | + render: (r) => ( | ||
| 29 | + <Link to={`/work-orders/${r.id}`} className="font-mono text-brand-600 hover:underline"> | ||
| 30 | + {r.code} | ||
| 31 | + </Link> | ||
| 32 | + ), | ||
| 33 | + }, | ||
| 34 | + { header: 'Output', key: 'outputItemCode', render: (r) => <span className="font-mono">{r.outputItemCode}</span> }, | ||
| 35 | + { | ||
| 36 | + header: 'Qty', | ||
| 37 | + key: 'outputQuantity', | ||
| 38 | + render: (r) => <span className="font-mono tabular-nums">{String(r.outputQuantity)}</span>, | ||
| 39 | + }, | ||
| 40 | + { header: 'Status', key: 'status', render: (r) => <StatusBadge status={r.status} /> }, | ||
| 41 | + { | ||
| 42 | + header: 'Source SO', | ||
| 43 | + key: 'sourceSalesOrderCode', | ||
| 44 | + render: (r) => r.sourceSalesOrderCode ?? '—', | ||
| 45 | + }, | ||
| 46 | + { | ||
| 47 | + header: 'Inputs / Ops', | ||
| 48 | + key: 'opsCount', | ||
| 49 | + render: (r) => `${r.inputs.length} / ${r.operations.length}`, | ||
| 50 | + }, | ||
| 51 | + ] | ||
| 52 | + | ||
| 53 | + return ( | ||
| 54 | + <div> | ||
| 55 | + <PageHeader title="Work Orders" subtitle="Production orders the framework consumes inputs and produces outputs through." /> | ||
| 56 | + {loading && <Loading />} | ||
| 57 | + {error && <ErrorBox error={error} />} | ||
| 58 | + {!loading && !error && <DataTable rows={rows} columns={columns} />} | ||
| 59 | + </div> | ||
| 60 | + ) | ||
| 61 | +} |
web/src/types/api.ts
0 → 100644
| 1 | +// vibe_erp REST API types. | ||
| 2 | +// | ||
| 3 | +// Hand-written rather than codegen'd from /v3/api-docs to keep the | ||
| 4 | +// build pipeline simple — the framework already has springdoc-openapi | ||
| 5 | +// serving the live spec, but adding @openapitools/openapi-generator-cli | ||
| 6 | +// would pull in another Java toolchain into the npm build. v1 SPA can | ||
| 7 | +// stay typed by hand; v1.x can revisit codegen if drift becomes a | ||
| 8 | +// real problem. Every type here mirrors the matching @RestController's | ||
| 9 | +// response DTO under pbc/*/http/. | ||
| 10 | +// | ||
| 11 | +// **BigDecimal as string.** The Spring Boot Jackson default serializes | ||
| 12 | +// java.math.BigDecimal as a JSON number, which JavaScript would coerce | ||
| 13 | +// to a 64-bit float and lose precision on quantities like 12345.6789. | ||
| 14 | +// In practice the framework configures `spring.jackson.write-bigdecimal-as-plain` | ||
| 15 | +// (default for Spring Boot 3) which still emits a JSON *number*; the SPA | ||
| 16 | +// stores them as `string` because we never do client-side arithmetic on | ||
| 17 | +// them — display only. If a future SPA needs sums it should round-trip | ||
| 18 | +// through decimal.js, not Number(). | ||
| 19 | + | ||
| 20 | +export interface MetaInfo { | ||
| 21 | + name: string | ||
| 22 | + apiVersion: string | ||
| 23 | + implementationVersion: string | ||
| 24 | + buildTime: string | null | ||
| 25 | + activeProfiles: string[] | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +// ─── Auth (pbc-identity AuthController) ────────────────────────────── | ||
| 29 | + | ||
| 30 | +export interface LoginRequest { | ||
| 31 | + username: string | ||
| 32 | + password: string | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +export interface TokenPair { | ||
| 36 | + accessToken: string | ||
| 37 | + accessExpiresAt: string | ||
| 38 | + refreshToken: string | ||
| 39 | + refreshExpiresAt: string | ||
| 40 | + tokenType: string | ||
| 41 | +} | ||
| 42 | + | ||
| 43 | +// ─── Catalog (pbc-catalog) ─────────────────────────────────────────── | ||
| 44 | + | ||
| 45 | +export type ItemType = 'GOOD' | 'SERVICE' | 'DIGITAL' | ||
| 46 | + | ||
| 47 | +export interface Item { | ||
| 48 | + id: string | ||
| 49 | + code: string | ||
| 50 | + name: string | ||
| 51 | + description: string | null | ||
| 52 | + itemType: ItemType | ||
| 53 | + baseUomCode: string | ||
| 54 | + active: boolean | ||
| 55 | + ext: Record<string, unknown> | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +export interface Uom { | ||
| 59 | + id: string | ||
| 60 | + code: string | ||
| 61 | + name: string | ||
| 62 | + dimension: string | ||
| 63 | +} | ||
| 64 | + | ||
| 65 | +// ─── Partners (pbc-partners) ───────────────────────────────────────── | ||
| 66 | + | ||
| 67 | +export type PartnerType = 'CUSTOMER' | 'SUPPLIER' | 'BOTH' | ||
| 68 | + | ||
| 69 | +export interface Partner { | ||
| 70 | + id: string | ||
| 71 | + code: string | ||
| 72 | + name: string | ||
| 73 | + type: PartnerType | ||
| 74 | + taxId: string | null | ||
| 75 | + website: string | null | ||
| 76 | + email: string | null | ||
| 77 | + phone: string | null | ||
| 78 | + active: boolean | ||
| 79 | + ext: Record<string, unknown> | ||
| 80 | +} | ||
| 81 | + | ||
| 82 | +// ─── Inventory (pbc-inventory) ─────────────────────────────────────── | ||
| 83 | + | ||
| 84 | +export type LocationType = 'WAREHOUSE' | 'BIN' | 'VIRTUAL' | ||
| 85 | + | ||
| 86 | +export interface Location { | ||
| 87 | + id: string | ||
| 88 | + code: string | ||
| 89 | + name: string | ||
| 90 | + type: LocationType | ||
| 91 | + active: boolean | ||
| 92 | + ext: Record<string, unknown> | ||
| 93 | +} | ||
| 94 | + | ||
| 95 | +// Note: balances/movements use a UUID `locationId`, not `locationCode`. | ||
| 96 | +// The list pages join against `Location.id → code` client-side. | ||
| 97 | + | ||
| 98 | +export interface StockBalance { | ||
| 99 | + id: string | ||
| 100 | + itemCode: string | ||
| 101 | + locationId: string | ||
| 102 | + quantity: string | number | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +export type MovementReason = | ||
| 106 | + | 'RECEIPT' | ||
| 107 | + | 'ISSUE' | ||
| 108 | + | 'ADJUSTMENT' | ||
| 109 | + | 'SALES_SHIPMENT' | ||
| 110 | + | 'PURCHASE_RECEIPT' | ||
| 111 | + | 'TRANSFER_OUT' | ||
| 112 | + | 'TRANSFER_IN' | ||
| 113 | + | 'MATERIAL_ISSUE' | ||
| 114 | + | 'PRODUCTION_RECEIPT' | ||
| 115 | + | ||
| 116 | +export interface StockMovement { | ||
| 117 | + id: string | ||
| 118 | + itemCode: string | ||
| 119 | + locationId: string | ||
| 120 | + delta: string | number | ||
| 121 | + reason: MovementReason | ||
| 122 | + reference: string | null | ||
| 123 | + occurredAt: string | ||
| 124 | + resultingQuantity?: string | number | ||
| 125 | +} | ||
| 126 | + | ||
| 127 | +// ─── Sales orders (pbc-orders-sales) ───────────────────────────────── | ||
| 128 | + | ||
| 129 | +export type SalesOrderStatus = 'DRAFT' | 'CONFIRMED' | 'SHIPPED' | 'CANCELLED' | ||
| 130 | + | ||
| 131 | +export interface SalesOrderLine { | ||
| 132 | + id: string | ||
| 133 | + lineNo: number | ||
| 134 | + itemCode: string | ||
| 135 | + quantity: string | number | ||
| 136 | + unitPrice: string | number | ||
| 137 | + currencyCode: string | ||
| 138 | + lineTotal: string | number | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +export interface SalesOrder { | ||
| 142 | + id: string | ||
| 143 | + code: string | ||
| 144 | + partnerCode: string | ||
| 145 | + status: SalesOrderStatus | ||
| 146 | + orderDate: string | ||
| 147 | + currencyCode: string | ||
| 148 | + totalAmount: string | number | ||
| 149 | + lines: SalesOrderLine[] | ||
| 150 | + ext: Record<string, unknown> | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +// ─── Purchase orders (pbc-orders-purchase) ─────────────────────────── | ||
| 154 | + | ||
| 155 | +export type PurchaseOrderStatus = 'DRAFT' | 'CONFIRMED' | 'RECEIVED' | 'CANCELLED' | ||
| 156 | + | ||
| 157 | +export interface PurchaseOrderLine { | ||
| 158 | + id: string | ||
| 159 | + lineNo: number | ||
| 160 | + itemCode: string | ||
| 161 | + quantity: string | number | ||
| 162 | + unitPrice: string | number | ||
| 163 | + currencyCode: string | ||
| 164 | + lineTotal: string | number | ||
| 165 | +} | ||
| 166 | + | ||
| 167 | +export interface PurchaseOrder { | ||
| 168 | + id: string | ||
| 169 | + code: string | ||
| 170 | + partnerCode: string | ||
| 171 | + status: PurchaseOrderStatus | ||
| 172 | + orderDate: string | ||
| 173 | + expectedDate: string | null | ||
| 174 | + currencyCode: string | ||
| 175 | + totalAmount: string | number | ||
| 176 | + lines: PurchaseOrderLine[] | ||
| 177 | + ext: Record<string, unknown> | ||
| 178 | +} | ||
| 179 | + | ||
| 180 | +// ─── Production (pbc-production) ───────────────────────────────────── | ||
| 181 | + | ||
| 182 | +export type WorkOrderStatus = 'DRAFT' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED' | ||
| 183 | +export type WorkOrderOperationStatus = 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | ||
| 184 | + | ||
| 185 | +export interface WorkOrderInput { | ||
| 186 | + id: string | ||
| 187 | + lineNo: number | ||
| 188 | + itemCode: string | ||
| 189 | + quantityPerUnit: string | number | ||
| 190 | + sourceLocationCode: string | ||
| 191 | +} | ||
| 192 | + | ||
| 193 | +export interface WorkOrderOperation { | ||
| 194 | + id: string | ||
| 195 | + lineNo: number | ||
| 196 | + operationCode: string | ||
| 197 | + workCenter: string | ||
| 198 | + standardMinutes: string | number | ||
| 199 | + status: WorkOrderOperationStatus | ||
| 200 | + actualMinutes: string | number | null | ||
| 201 | + startedAt: string | null | ||
| 202 | + completedAt: string | null | ||
| 203 | +} | ||
| 204 | + | ||
| 205 | +export interface WorkOrder { | ||
| 206 | + id: string | ||
| 207 | + code: string | ||
| 208 | + outputItemCode: string | ||
| 209 | + outputQuantity: string | number | ||
| 210 | + status: WorkOrderStatus | ||
| 211 | + dueDate: string | null | ||
| 212 | + sourceSalesOrderCode: string | null | ||
| 213 | + inputs: WorkOrderInput[] | ||
| 214 | + operations: WorkOrderOperation[] | ||
| 215 | + ext: Record<string, unknown> | ||
| 216 | +} | ||
| 217 | + | ||
| 218 | +export interface ShopFloorEntry { | ||
| 219 | + workOrderId: string | ||
| 220 | + workOrderCode: string | ||
| 221 | + outputItemCode: string | ||
| 222 | + outputQuantity: string | number | ||
| 223 | + sourceSalesOrderCode: string | null | ||
| 224 | + currentOperationLineNo: number | null | ||
| 225 | + currentOperationCode: string | null | ||
| 226 | + currentWorkCenter: string | null | ||
| 227 | + currentOperationStatus: WorkOrderOperationStatus | null | ||
| 228 | + operationsCompleted: number | ||
| 229 | + operationsTotal: number | ||
| 230 | + totalStandardMinutes: string | number | ||
| 231 | + totalActualMinutes: string | number | ||
| 232 | +} | ||
| 233 | + | ||
| 234 | +// ─── Finance (pbc-finance) ─────────────────────────────────────────── | ||
| 235 | + | ||
| 236 | +export type JournalEntryType = 'AR' | 'AP' | ||
| 237 | +export type JournalEntryStatus = 'POSTED' | 'SETTLED' | 'REVERSED' | ||
| 238 | + | ||
| 239 | +export interface JournalEntry { | ||
| 240 | + id: string | ||
| 241 | + code: string | ||
| 242 | + type: JournalEntryType | ||
| 243 | + status: JournalEntryStatus | ||
| 244 | + partnerCode: string | ||
| 245 | + orderCode: string | ||
| 246 | + amount: string | number | ||
| 247 | + currencyCode: string | ||
| 248 | + postedAt: string | ||
| 249 | +} |
web/tailwind.config.js
0 → 100644
| 1 | +/** @type {import('tailwindcss').Config} */ | ||
| 2 | +export default { | ||
| 3 | + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], | ||
| 4 | + theme: { | ||
| 5 | + extend: { | ||
| 6 | + colors: { | ||
| 7 | + brand: { | ||
| 8 | + 50: '#eef4ff', | ||
| 9 | + 100: '#d9e6ff', | ||
| 10 | + 500: '#3b6cf6', | ||
| 11 | + 600: '#2a55d4', | ||
| 12 | + 700: '#1f43a8', | ||
| 13 | + }, | ||
| 14 | + }, | ||
| 15 | + fontFamily: { | ||
| 16 | + sans: [ | ||
| 17 | + 'Inter', | ||
| 18 | + 'system-ui', | ||
| 19 | + '-apple-system', | ||
| 20 | + 'Segoe UI', | ||
| 21 | + 'Roboto', | ||
| 22 | + 'sans-serif', | ||
| 23 | + ], | ||
| 24 | + mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'], | ||
| 25 | + }, | ||
| 26 | + }, | ||
| 27 | + }, | ||
| 28 | + plugins: [], | ||
| 29 | +} |
web/tsconfig.json
0 → 100644
| 1 | +{ | ||
| 2 | + "compilerOptions": { | ||
| 3 | + "target": "ES2022", | ||
| 4 | + "useDefineForClassFields": true, | ||
| 5 | + "lib": ["ES2022", "DOM", "DOM.Iterable"], | ||
| 6 | + "module": "ESNext", | ||
| 7 | + "skipLibCheck": true, | ||
| 8 | + | ||
| 9 | + "moduleResolution": "bundler", | ||
| 10 | + "allowImportingTsExtensions": true, | ||
| 11 | + "isolatedModules": true, | ||
| 12 | + "moduleDetection": "force", | ||
| 13 | + "noEmit": true, | ||
| 14 | + "jsx": "react-jsx", | ||
| 15 | + | ||
| 16 | + "strict": true, | ||
| 17 | + "noUnusedLocals": true, | ||
| 18 | + "noUnusedParameters": true, | ||
| 19 | + "noFallthroughCasesInSwitch": true, | ||
| 20 | + "esModuleInterop": true, | ||
| 21 | + "resolveJsonModule": true, | ||
| 22 | + | ||
| 23 | + "baseUrl": ".", | ||
| 24 | + "paths": { | ||
| 25 | + "@/*": ["src/*"] | ||
| 26 | + } | ||
| 27 | + }, | ||
| 28 | + "include": ["src"], | ||
| 29 | + "references": [{ "path": "./tsconfig.node.json" }] | ||
| 30 | +} |
web/tsconfig.node.json
0 → 100644
| 1 | +{ | ||
| 2 | + "compilerOptions": { | ||
| 3 | + "composite": true, | ||
| 4 | + "skipLibCheck": true, | ||
| 5 | + "module": "ESNext", | ||
| 6 | + "moduleResolution": "bundler", | ||
| 7 | + "allowSyntheticDefaultImports": true, | ||
| 8 | + "esModuleInterop": true, | ||
| 9 | + "strict": true, | ||
| 10 | + "types": ["node"] | ||
| 11 | + }, | ||
| 12 | + "include": ["vite.config.ts"] | ||
| 13 | +} |
web/vite.config.ts
0 → 100644
| 1 | +import { defineConfig } from 'vite' | ||
| 2 | +import react from '@vitejs/plugin-react' | ||
| 3 | +import path from 'node:path' | ||
| 4 | + | ||
| 5 | +// vibe_erp web SPA — Vite config. | ||
| 6 | +// | ||
| 7 | +// **Why this lives at web/, not under platform-bootstrap/static/** | ||
| 8 | +// Keeping the SPA in its own directory means a frontend dev can run | ||
| 9 | +// `npm run dev` (Vite dev server on :5173) without touching the JVM | ||
| 10 | +// build, while the production build pipeline drops the static bundle | ||
| 11 | +// into distribution/src/main/resources/static/ for Spring Boot to | ||
| 12 | +// serve. The Gradle wrapper task in web/build.gradle.kts is what | ||
| 13 | +// glues `npm run build` into `./gradlew build` so a normal CI run | ||
| 14 | +// produces a single fat-jar that already contains the SPA. | ||
| 15 | +// | ||
| 16 | +// **Dev proxy.** During `npm run dev` the Vite server proxies every | ||
| 17 | +// /api/v1/** call to the locally-running Spring Boot at :8080 so | ||
| 18 | +// the SPA dev experience is "edit a .tsx file, save, hot-reload, | ||
| 19 | +// the same JWT keeps working against the real backend". The proxy | ||
| 20 | +// is dev-only — in the production bundle the SPA and API live on | ||
| 21 | +// the same origin so no proxy is needed. | ||
| 22 | +export default defineConfig({ | ||
| 23 | + plugins: [react()], | ||
| 24 | + resolve: { | ||
| 25 | + alias: { | ||
| 26 | + '@': path.resolve(__dirname, './src'), | ||
| 27 | + }, | ||
| 28 | + }, | ||
| 29 | + server: { | ||
| 30 | + port: 5173, | ||
| 31 | + proxy: { | ||
| 32 | + '/api': { | ||
| 33 | + target: 'http://localhost:8080', | ||
| 34 | + changeOrigin: true, | ||
| 35 | + }, | ||
| 36 | + }, | ||
| 37 | + }, | ||
| 38 | + build: { | ||
| 39 | + outDir: 'dist', | ||
| 40 | + sourcemap: true, | ||
| 41 | + }, | ||
| 42 | +}) |
-
mentioned in commit 87399862