@open-mercato/core 0.6.8-develop.7082.1.f2a845b020 → 0.6.8-develop.7090.1.4dbca7b350
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/components/DealsKpiStrip.js +7 -6
- package/dist/modules/customers/components/DealsKpiStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +5 -4
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +6 -5
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/components/detail/utils.js +5 -5
- package/dist/modules/customers/components/detail/utils.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +10 -8
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/components/PriceWithCurrency.js +7 -5
- package/dist/modules/sales/components/PriceWithCurrency.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentsSection.js +8 -6
- package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +14 -7
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +8 -7
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +9 -8
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnsSection.js +8 -6
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +5 -4
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +5 -4
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +8 -6
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/sales/data/entities.js +8 -2
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js +32 -0
- package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js.map +7 -0
- package/dist/modules/staff/backend/staff/timesheets/page.js +29 -18
- package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/customers/components/DealsKpiStrip.tsx +7 -7
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +5 -4
- package/src/modules/customers/components/detail/DealsSection.tsx +11 -5
- package/src/modules/customers/components/detail/utils.ts +15 -5
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +14 -8
- package/src/modules/sales/components/PriceWithCurrency.tsx +17 -5
- package/src/modules/sales/components/documents/AdjustmentsSection.tsx +6 -5
- package/src/modules/sales/components/documents/ItemsSection.tsx +8 -1
- package/src/modules/sales/components/documents/LineItemDialog.tsx +9 -8
- package/src/modules/sales/components/documents/PaymentsSection.tsx +9 -8
- package/src/modules/sales/components/documents/ReturnsSection.tsx +6 -5
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +5 -4
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +5 -4
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +6 -5
- package/src/modules/sales/components/documents/lineItemUtils.ts +7 -2
- package/src/modules/sales/data/entities.ts +6 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +60 -0
- package/src/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.ts +87 -0
- package/src/modules/staff/backend/staff/timesheets/page.tsx +12 -1
|
@@ -10,7 +10,7 @@ import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/u
|
|
|
10
10
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
11
11
|
import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
12
12
|
import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
13
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
13
|
+
import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
|
|
14
14
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
15
15
|
import {
|
|
16
16
|
emitSalesDocumentTotalsRefresh,
|
|
@@ -41,11 +41,11 @@ type SalesShipmentsSectionProps = {
|
|
|
41
41
|
onAddComment?: (body: string) => Promise<void>
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function formatDisplayDate(value: string | null | undefined): string | null {
|
|
44
|
+
export function formatDisplayDate(value: string | null | undefined, locale?: string): string | null {
|
|
45
45
|
if (!value) return null
|
|
46
46
|
const date = new Date(value)
|
|
47
47
|
if (Number.isNaN(date.getTime())) return null
|
|
48
|
-
return new Intl.DateTimeFormat(
|
|
48
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const formatShipmentAddress = (metadata?: Record<string, unknown> | null): string | null => {
|
|
@@ -123,6 +123,7 @@ export function SalesShipmentsSection({
|
|
|
123
123
|
onAddComment,
|
|
124
124
|
}: SalesShipmentsSectionProps) {
|
|
125
125
|
const t = useT()
|
|
126
|
+
const locale = useLocale()
|
|
126
127
|
const { organizationId, tenantId } = useOrganizationScopeDetail()
|
|
127
128
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
128
129
|
const resolvedOrganizationId = organizationIdProp ?? organizationId ?? null
|
|
@@ -486,8 +487,8 @@ export function SalesShipmentsSection({
|
|
|
486
487
|
) : (
|
|
487
488
|
<div className="grid gap-3 md:grid-cols-2">
|
|
488
489
|
{shipments.map((shipment) => {
|
|
489
|
-
const shippedAt = formatDisplayDate(shipment.shippedAt)
|
|
490
|
-
const deliveredAt = formatDisplayDate(shipment.deliveredAt)
|
|
490
|
+
const shippedAt = formatDisplayDate(shipment.shippedAt, locale)
|
|
491
|
+
const deliveredAt = formatDisplayDate(shipment.deliveredAt, locale)
|
|
491
492
|
const addressSummary = formatShipmentAddress(shipment.metadata)
|
|
492
493
|
const statusLabel =
|
|
493
494
|
shipment.statusLabel ??
|
|
@@ -9,9 +9,14 @@ export function normalizeNumber(value: unknown, fallback = 0): number {
|
|
|
9
9
|
return fallback
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Pass `locale` to format in the application locale (`useLocale()` in client components).
|
|
14
|
+
* Omitting it keeps the runtime default, which varies per machine and is therefore not
|
|
15
|
+
* assertable in tests.
|
|
16
|
+
*/
|
|
17
|
+
export function formatMoney(value: number, currency: string | null | undefined, locale?: string): string {
|
|
13
18
|
if (!currency) return value.toFixed(2)
|
|
14
|
-
return new Intl.NumberFormat(
|
|
19
|
+
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value)
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
export type LineDiscountDisplay = {
|
|
@@ -1790,6 +1790,9 @@ export class SalesPaymentAllocation {
|
|
|
1790
1790
|
|
|
1791
1791
|
@Entity({ tableName: 'sales_notes' })
|
|
1792
1792
|
@Index({ name: 'sales_notes_scope_idx', properties: ['organizationId', 'tenantId'] })
|
|
1793
|
+
@Index({ name: 'sales_notes_context_idx', properties: ['contextId'] })
|
|
1794
|
+
@Index({ name: 'sales_notes_order_idx', properties: ['order'] })
|
|
1795
|
+
@Index({ name: 'sales_notes_quote_idx', properties: ['quote'] })
|
|
1793
1796
|
export class SalesNote {
|
|
1794
1797
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
1795
1798
|
id!: string
|
|
@@ -1836,6 +1839,9 @@ export class SalesNote {
|
|
|
1836
1839
|
|
|
1837
1840
|
@Entity({ tableName: 'sales_document_addresses' })
|
|
1838
1841
|
@Index({ name: 'sales_document_addresses_scope_idx', properties: ['organizationId', 'tenantId'] })
|
|
1842
|
+
@Index({ name: 'sales_document_addresses_document_idx', properties: ['documentId'] })
|
|
1843
|
+
@Index({ name: 'sales_document_addresses_order_idx', properties: ['order'] })
|
|
1844
|
+
@Index({ name: 'sales_document_addresses_quote_idx', properties: ['quote'] })
|
|
1839
1845
|
export class SalesDocumentAddress {
|
|
1840
1846
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
1841
1847
|
id!: string
|
|
@@ -1962,6 +1962,36 @@
|
|
|
1962
1962
|
"keyName": "sales_document_addresses_scope_idx",
|
|
1963
1963
|
"primary": false,
|
|
1964
1964
|
"unique": false
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
"columnNames": [
|
|
1968
|
+
"document_id"
|
|
1969
|
+
],
|
|
1970
|
+
"composite": false,
|
|
1971
|
+
"constraint": false,
|
|
1972
|
+
"keyName": "sales_document_addresses_document_idx",
|
|
1973
|
+
"primary": false,
|
|
1974
|
+
"unique": false
|
|
1975
|
+
},
|
|
1976
|
+
{
|
|
1977
|
+
"columnNames": [
|
|
1978
|
+
"order_id"
|
|
1979
|
+
],
|
|
1980
|
+
"composite": false,
|
|
1981
|
+
"constraint": false,
|
|
1982
|
+
"keyName": "sales_document_addresses_order_idx",
|
|
1983
|
+
"primary": false,
|
|
1984
|
+
"unique": false
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
"columnNames": [
|
|
1988
|
+
"quote_id"
|
|
1989
|
+
],
|
|
1990
|
+
"composite": false,
|
|
1991
|
+
"constraint": false,
|
|
1992
|
+
"keyName": "sales_document_addresses_quote_idx",
|
|
1993
|
+
"primary": false,
|
|
1994
|
+
"unique": false
|
|
1965
1995
|
}
|
|
1966
1996
|
],
|
|
1967
1997
|
"checks": [],
|
|
@@ -3750,6 +3780,36 @@
|
|
|
3750
3780
|
"keyName": "sales_notes_scope_idx",
|
|
3751
3781
|
"primary": false,
|
|
3752
3782
|
"unique": false
|
|
3783
|
+
},
|
|
3784
|
+
{
|
|
3785
|
+
"columnNames": [
|
|
3786
|
+
"context_id"
|
|
3787
|
+
],
|
|
3788
|
+
"composite": false,
|
|
3789
|
+
"constraint": false,
|
|
3790
|
+
"keyName": "sales_notes_context_idx",
|
|
3791
|
+
"primary": false,
|
|
3792
|
+
"unique": false
|
|
3793
|
+
},
|
|
3794
|
+
{
|
|
3795
|
+
"columnNames": [
|
|
3796
|
+
"order_id"
|
|
3797
|
+
],
|
|
3798
|
+
"composite": false,
|
|
3799
|
+
"constraint": false,
|
|
3800
|
+
"keyName": "sales_notes_order_idx",
|
|
3801
|
+
"primary": false,
|
|
3802
|
+
"unique": false
|
|
3803
|
+
},
|
|
3804
|
+
{
|
|
3805
|
+
"columnNames": [
|
|
3806
|
+
"quote_id"
|
|
3807
|
+
],
|
|
3808
|
+
"composite": false,
|
|
3809
|
+
"constraint": false,
|
|
3810
|
+
"keyName": "sales_notes_quote_idx",
|
|
3811
|
+
"primary": false,
|
|
3812
|
+
"unique": false
|
|
3753
3813
|
}
|
|
3754
3814
|
],
|
|
3755
3815
|
"checks": [],
|
package/src/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
// sales_notes and sales_document_addresses each carried a single index —
|
|
4
|
+
// (organization_id, tenant_id) — and are never read by that predicate alone.
|
|
5
|
+
// loadOrderSnapshot / loadQuoteSnapshot (commands/documents.ts) read them by
|
|
6
|
+
// context in the same Promise.all:
|
|
7
|
+
//
|
|
8
|
+
// findWithDecryption(em, SalesNote, { contextType, contextId, ...scope })
|
|
9
|
+
// findWithDecryption(em, SalesDocumentAddress, { documentId, documentKind, ...scope })
|
|
10
|
+
//
|
|
11
|
+
// On a single-organization deployment the scope index matches every row, so the
|
|
12
|
+
// planner discards it and scans. The cost is CPU filtering, so a warm cache does
|
|
13
|
+
// not reduce it. Measured on a table of ~331k rows / 110 MB: a parallel
|
|
14
|
+
// sequential scan removing 110,457 rows per worker, 82 ms per read to return 0
|
|
15
|
+
// rows. The command bus runs prepare() and captureAfter() around every command
|
|
16
|
+
// and both load a document snapshot, so one written document — itself several
|
|
17
|
+
// commands (the document, one per changed line, the payment reconcile) —
|
|
18
|
+
// performs roughly 20 of these reads. Counters over one bulk-import batch of 100
|
|
19
|
+
// records showed 1,971 sequential scans reading 217,670,528 rows, 2.18M rows per
|
|
20
|
+
// imported record; repeatable across consecutive batches (1,971 / 1,974 / 1,911).
|
|
21
|
+
//
|
|
22
|
+
// context_type and document_kind are deliberately omitted, as they were for
|
|
23
|
+
// sales_document_tag_assignments_document_idx (Migration20260806120000), and the
|
|
24
|
+
// scope columns with them. sales_orders.id and sales_quotes.id are both
|
|
25
|
+
// gen_random_uuid() from their own tables, so a context_id already determines its
|
|
26
|
+
// context_type and its tenant; the seek returns the handful of rows belonging to
|
|
27
|
+
// one document and the remaining terms are rechecked over those. Folding three
|
|
28
|
+
// more columns in — one text and two uuids — roughly triples the index entry for
|
|
29
|
+
// no selectivity, on tables that take a write per document save and a note per
|
|
30
|
+
// order status change. The sibling scope indexes that do fold scope in
|
|
31
|
+
// (sales_order_lines_scope_idx, sales_payments_scope_idx) predate that reasoning.
|
|
32
|
+
//
|
|
33
|
+
// order_id and quote_id are separate indexes rather than a widening of the above:
|
|
34
|
+
// both tables are the child side of `on delete set null` constraints, which
|
|
35
|
+
// PostgreSQL does not index automatically, and the constraint check probes
|
|
36
|
+
// order_id / quote_id — columns the context index cannot serve even though they
|
|
37
|
+
// hold the same uuid. Without them every delete of a parent order or quote scans
|
|
38
|
+
// the child table in full.
|
|
39
|
+
//
|
|
40
|
+
// Both tables are high-churn, so the indexes are built CONCURRENTLY to avoid
|
|
41
|
+
// blocking writes during the build. CREATE INDEX CONCURRENTLY cannot run inside a
|
|
42
|
+
// transaction, hence isTransactional() => false; the migration runner applies
|
|
43
|
+
// migrations one-by-one, so this opt-out is safe. Drop first so retrying a failed
|
|
44
|
+
// concurrent build removes PostgreSQL's invalid index stub instead of letting IF
|
|
45
|
+
// NOT EXISTS silently accept it: a half-built concurrent index is left INVALID,
|
|
46
|
+
// the planner ignores it, and IF NOT EXISTS would report success while the table
|
|
47
|
+
// stays unindexed. Same shape as Migration20260806120000 and
|
|
48
|
+
// Migration20260731105052_query_index.
|
|
49
|
+
//
|
|
50
|
+
// On a database that does not have these indexes yet — every fresh install — the
|
|
51
|
+
// drops are no-ops and there is no window without them. An operator who already
|
|
52
|
+
// created one out of band should expect it to be rebuilt here, and the reads that
|
|
53
|
+
// depend on it to fall back to their pre-index cost until the concurrent build
|
|
54
|
+
// finishes; on a large table, run this while the writers that depend on it are
|
|
55
|
+
// paused.
|
|
56
|
+
export class Migration20260821120000_sales_notes_addresses_context_idx extends Migration {
|
|
57
|
+
|
|
58
|
+
override isTransactional(): boolean {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override up(): void | Promise<void> {
|
|
63
|
+
this.addSql(`drop index concurrently if exists "sales_notes_context_idx";`);
|
|
64
|
+
this.addSql(`create index concurrently "sales_notes_context_idx" on "sales_notes" ("context_id");`);
|
|
65
|
+
this.addSql(`drop index concurrently if exists "sales_notes_order_idx";`);
|
|
66
|
+
this.addSql(`create index concurrently "sales_notes_order_idx" on "sales_notes" ("order_id");`);
|
|
67
|
+
this.addSql(`drop index concurrently if exists "sales_notes_quote_idx";`);
|
|
68
|
+
this.addSql(`create index concurrently "sales_notes_quote_idx" on "sales_notes" ("quote_id");`);
|
|
69
|
+
|
|
70
|
+
this.addSql(`drop index concurrently if exists "sales_document_addresses_document_idx";`);
|
|
71
|
+
this.addSql(`create index concurrently "sales_document_addresses_document_idx" on "sales_document_addresses" ("document_id");`);
|
|
72
|
+
this.addSql(`drop index concurrently if exists "sales_document_addresses_order_idx";`);
|
|
73
|
+
this.addSql(`create index concurrently "sales_document_addresses_order_idx" on "sales_document_addresses" ("order_id");`);
|
|
74
|
+
this.addSql(`drop index concurrently if exists "sales_document_addresses_quote_idx";`);
|
|
75
|
+
this.addSql(`create index concurrently "sales_document_addresses_quote_idx" on "sales_document_addresses" ("quote_id");`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override down(): void | Promise<void> {
|
|
79
|
+
this.addSql(`drop index concurrently if exists "sales_document_addresses_quote_idx";`);
|
|
80
|
+
this.addSql(`drop index concurrently if exists "sales_document_addresses_order_idx";`);
|
|
81
|
+
this.addSql(`drop index concurrently if exists "sales_document_addresses_document_idx";`);
|
|
82
|
+
this.addSql(`drop index concurrently if exists "sales_notes_quote_idx";`);
|
|
83
|
+
this.addSql(`drop index concurrently if exists "sales_notes_order_idx";`);
|
|
84
|
+
this.addSql(`drop index concurrently if exists "sales_notes_context_idx";`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
}
|
|
@@ -178,6 +178,12 @@ export default function MyTimesheetsPage() {
|
|
|
178
178
|
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
179
179
|
})
|
|
180
180
|
|
|
181
|
+
const gridInstanceId = React.useId()
|
|
182
|
+
const durationHintId = `${gridInstanceId}-duration-hint`
|
|
183
|
+
const buildDurationErrorId = React.useCallback(
|
|
184
|
+
(projectId: string, dateKey: string): string => `${gridInstanceId}-duration-error-${projectId}-${dateKey}`,
|
|
185
|
+
[gridInstanceId],
|
|
186
|
+
)
|
|
181
187
|
const durationFormatHint = t(
|
|
182
188
|
'staff.timesheets.my.duration.hint',
|
|
183
189
|
'Enter hours (8 or 1.5), h:mm (1:30) or minutes (90m). Max 24h per day.',
|
|
@@ -919,6 +925,7 @@ export default function MyTimesheetsPage() {
|
|
|
919
925
|
const isDirty = dirty[project.id]?.[dateKey] !== undefined
|
|
920
926
|
const cellError = cellErrors[project.id]?.[dateKey]
|
|
921
927
|
const cellErrorMessage = cellError ? describeDurationError(cellError) : undefined
|
|
928
|
+
const cellErrorId = cellErrorMessage ? buildDurationErrorId(project.id, dateKey) : undefined
|
|
922
929
|
return (
|
|
923
930
|
<td key={dateKey} className={`px-0.5 py-0.5 ${weekend ? 'bg-muted/40' : ''}`}>
|
|
924
931
|
{weekend ? (
|
|
@@ -943,9 +950,13 @@ export default function MyTimesheetsPage() {
|
|
|
943
950
|
placeholder={t('staff.timesheets.my.durationPlaceholder', '0')}
|
|
944
951
|
aria-invalid={cellError !== undefined}
|
|
945
952
|
aria-label={describeDurationCell(project.name, date)}
|
|
953
|
+
aria-describedby={[durationHintId, cellErrorId].filter(Boolean).join(' ')}
|
|
946
954
|
title={cellErrorMessage ?? durationFormatHint}
|
|
947
955
|
/>
|
|
948
956
|
)}
|
|
957
|
+
{cellErrorMessage ? (
|
|
958
|
+
<span id={cellErrorId} role="alert" className="sr-only">{cellErrorMessage}</span>
|
|
959
|
+
) : null}
|
|
949
960
|
</td>
|
|
950
961
|
)
|
|
951
962
|
})}
|
|
@@ -984,7 +995,7 @@ export default function MyTimesheetsPage() {
|
|
|
984
995
|
</tr>
|
|
985
996
|
</tfoot>
|
|
986
997
|
</table>
|
|
987
|
-
<p className="px-3 py-2 text-xs text-muted-foreground">{durationFormatHint}</p>
|
|
998
|
+
<p id={durationHintId} className="px-3 py-2 text-xs text-muted-foreground">{durationFormatHint}</p>
|
|
988
999
|
</div>
|
|
989
1000
|
)}
|
|
990
1001
|
</div>
|