@voyant-travel/quotes-react 0.119.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/dist/client.d.ts +31 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +71 -0
  6. package/dist/components/create-quote-dialog.d.ts +10 -0
  7. package/dist/components/create-quote-dialog.d.ts.map +1 -0
  8. package/dist/components/create-quote-dialog.js +45 -0
  9. package/dist/components/create-quote-version-dialog.d.ts +9 -0
  10. package/dist/components/create-quote-version-dialog.d.ts.map +1 -0
  11. package/dist/components/create-quote-version-dialog.js +108 -0
  12. package/dist/components/crm-format.d.ts +7 -0
  13. package/dist/components/crm-format.d.ts.map +1 -0
  14. package/dist/components/crm-format.js +40 -0
  15. package/dist/components/inline-currency-field.d.ts +12 -0
  16. package/dist/components/inline-currency-field.d.ts.map +1 -0
  17. package/dist/components/inline-currency-field.js +46 -0
  18. package/dist/components/inline-field.d.ts +16 -0
  19. package/dist/components/inline-field.d.ts.map +1 -0
  20. package/dist/components/inline-field.js +45 -0
  21. package/dist/components/inline-language-field.d.ts +12 -0
  22. package/dist/components/inline-language-field.d.ts.map +1 -0
  23. package/dist/components/inline-language-field.js +46 -0
  24. package/dist/components/inline-number-field.d.ts +15 -0
  25. package/dist/components/inline-number-field.d.ts.map +1 -0
  26. package/dist/components/inline-number-field.js +58 -0
  27. package/dist/components/inline-select-field.d.ts +19 -0
  28. package/dist/components/inline-select-field.d.ts.map +1 -0
  29. package/dist/components/inline-select-field.js +34 -0
  30. package/dist/components/quote-summary-card.d.ts +11 -0
  31. package/dist/components/quote-summary-card.d.ts.map +1 -0
  32. package/dist/components/quote-summary-card.js +11 -0
  33. package/dist/components/quote-version-detail-sections.d.ts +22 -0
  34. package/dist/components/quote-version-detail-sections.d.ts.map +1 -0
  35. package/dist/components/quote-version-detail-sections.js +87 -0
  36. package/dist/components/quote-versions-page.d.ts +8 -0
  37. package/dist/components/quote-versions-page.d.ts.map +1 -0
  38. package/dist/components/quote-versions-page.js +30 -0
  39. package/dist/components/quotes-board.d.ts +8 -0
  40. package/dist/components/quotes-board.d.ts.map +1 -0
  41. package/dist/components/quotes-board.js +24 -0
  42. package/dist/hooks/index.d.ts +10 -0
  43. package/dist/hooks/index.d.ts.map +1 -0
  44. package/dist/hooks/index.js +9 -0
  45. package/dist/hooks/use-pipeline-mutation.d.ts +76 -0
  46. package/dist/hooks/use-pipeline-mutation.d.ts.map +1 -0
  47. package/dist/hooks/use-pipeline-mutation.js +76 -0
  48. package/dist/hooks/use-pipelines.d.ts +31 -0
  49. package/dist/hooks/use-pipelines.d.ts.map +1 -0
  50. package/dist/hooks/use-pipelines.js +42 -0
  51. package/dist/hooks/use-quote-mutation.d.ts +72 -0
  52. package/dist/hooks/use-quote-mutation.d.ts.map +1 -0
  53. package/dist/hooks/use-quote-mutation.js +50 -0
  54. package/dist/hooks/use-quote-version-mutation.d.ts +272 -0
  55. package/dist/hooks/use-quote-version-mutation.d.ts.map +1 -0
  56. package/dist/hooks/use-quote-version-mutation.js +164 -0
  57. package/dist/hooks/use-quote-version.d.ts +37 -0
  58. package/dist/hooks/use-quote-version.d.ts.map +1 -0
  59. package/dist/hooks/use-quote-version.js +34 -0
  60. package/dist/hooks/use-quote-versions.d.ts +30 -0
  61. package/dist/hooks/use-quote-versions.d.ts.map +1 -0
  62. package/dist/hooks/use-quote-versions.js +27 -0
  63. package/dist/hooks/use-quote.d.ts +26 -0
  64. package/dist/hooks/use-quote.d.ts.map +1 -0
  65. package/dist/hooks/use-quote.js +23 -0
  66. package/dist/hooks/use-quotes.d.ts +36 -0
  67. package/dist/hooks/use-quotes.d.ts.map +1 -0
  68. package/dist/hooks/use-quotes.js +44 -0
  69. package/dist/hooks/use-stages.d.ts +37 -0
  70. package/dist/hooks/use-stages.d.ts.map +1 -0
  71. package/dist/hooks/use-stages.js +45 -0
  72. package/dist/i18n/en/base.d.ts +189 -0
  73. package/dist/i18n/en/base.d.ts.map +1 -0
  74. package/dist/i18n/en/base.js +188 -0
  75. package/dist/i18n/en/commerce.d.ts +133 -0
  76. package/dist/i18n/en/commerce.d.ts.map +1 -0
  77. package/dist/i18n/en/commerce.js +132 -0
  78. package/dist/i18n/en/detail.d.ts +211 -0
  79. package/dist/i18n/en/detail.d.ts.map +1 -0
  80. package/dist/i18n/en/detail.js +210 -0
  81. package/dist/i18n/en/lists.d.ts +75 -0
  82. package/dist/i18n/en/lists.d.ts.map +1 -0
  83. package/dist/i18n/en/lists.js +74 -0
  84. package/dist/i18n/en.d.ts +599 -0
  85. package/dist/i18n/en.d.ts.map +1 -0
  86. package/dist/i18n/en.js +10 -0
  87. package/dist/i18n/index.d.ts +5 -0
  88. package/dist/i18n/index.d.ts.map +1 -0
  89. package/dist/i18n/index.js +3 -0
  90. package/dist/i18n/messages.d.ts +577 -0
  91. package/dist/i18n/messages.d.ts.map +1 -0
  92. package/dist/i18n/messages.js +15 -0
  93. package/dist/i18n/provider.d.ts +1220 -0
  94. package/dist/i18n/provider.d.ts.map +1 -0
  95. package/dist/i18n/provider.js +44 -0
  96. package/dist/i18n/ro/base.d.ts +189 -0
  97. package/dist/i18n/ro/base.d.ts.map +1 -0
  98. package/dist/i18n/ro/base.js +188 -0
  99. package/dist/i18n/ro/commerce.d.ts +133 -0
  100. package/dist/i18n/ro/commerce.d.ts.map +1 -0
  101. package/dist/i18n/ro/commerce.js +132 -0
  102. package/dist/i18n/ro/detail.d.ts +211 -0
  103. package/dist/i18n/ro/detail.d.ts.map +1 -0
  104. package/dist/i18n/ro/detail.js +210 -0
  105. package/dist/i18n/ro/lists.d.ts +75 -0
  106. package/dist/i18n/ro/lists.d.ts.map +1 -0
  107. package/dist/i18n/ro/lists.js +74 -0
  108. package/dist/i18n/ro.d.ts +599 -0
  109. package/dist/i18n/ro.d.ts.map +1 -0
  110. package/dist/i18n/ro.js +10 -0
  111. package/dist/index.d.ts +22 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +21 -0
  114. package/dist/provider.d.ts +2 -0
  115. package/dist/provider.d.ts.map +1 -0
  116. package/dist/provider.js +1 -0
  117. package/dist/query-keys.d.ts +44 -0
  118. package/dist/query-keys.d.ts.map +1 -0
  119. package/dist/query-keys.js +16 -0
  120. package/dist/query-options.d.ts +678 -0
  121. package/dist/query-options.d.ts.map +1 -0
  122. package/dist/query-options.js +160 -0
  123. package/dist/schemas.d.ts +348 -0
  124. package/dist/schemas.d.ts.map +1 -0
  125. package/dist/schemas.js +101 -0
  126. package/dist/ui.d.ts +8 -0
  127. package/dist/ui.d.ts.map +1 -0
  128. package/dist/ui.js +7 -0
  129. package/package.json +117 -0
  130. package/src/styles.css +11 -0
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyant-travel/i18n";
4
+ import { cn } from "@voyant-travel/ui/components";
5
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyant-travel/ui/components/combobox";
6
+ import { currencies } from "@voyant-travel/utils/currencies";
7
+ import { Pencil } from "lucide-react";
8
+ import { useState } from "react";
9
+ import { useCrmUiMessagesOrDefault } from "../i18n/index.js";
10
+ const CURRENCY_CODES = Object.keys(currencies).sort();
11
+ export function InlineCurrencyField({ icon: Icon, label, value, disabled, onSave, }) {
12
+ const messages = useCrmUiMessagesOrDefault().inlineEditor;
13
+ const [editing, setEditing] = useState(false);
14
+ const [saving, setSaving] = useState(false);
15
+ const [error, setError] = useState(null);
16
+ async function commitSelection(next) {
17
+ setSaving(true);
18
+ setError(null);
19
+ try {
20
+ await onSave(next);
21
+ setEditing(false);
22
+ }
23
+ catch (err) {
24
+ setError(err instanceof Error ? err.message : messages.failedToSave);
25
+ }
26
+ finally {
27
+ setSaving(false);
28
+ }
29
+ }
30
+ const currencyInfo = value ? currencies[value] : null;
31
+ return (_jsxs("div", { className: "group flex items-start gap-3 py-1.5", children: [Icon ? _jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), editing ? (_jsxs(Combobox, { items: CURRENCY_CODES, defaultOpen: true, autoHighlight: true, filter: (code, query) => {
32
+ const currency = currencies[code];
33
+ if (!currency)
34
+ return false;
35
+ const normalized = query.toLowerCase();
36
+ return (currency.code.toLowerCase().includes(normalized) ||
37
+ currency.name.toLowerCase().includes(normalized) ||
38
+ currency.symbol.toLowerCase().includes(normalized));
39
+ }, onValueChange: (next) => void commitSelection(next ?? null), onOpenChange: (open) => {
40
+ if (!open)
41
+ setEditing(false);
42
+ }, children: [_jsx(ComboboxInput, { autoFocus: true, placeholder: messages.searchCurrencyPlaceholder, className: "mt-0.5 h-8 text-sm", disabled: saving }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: messages.noCurrenciesFound }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (code) => {
43
+ const currency = currencies[code];
44
+ return (_jsxs(ComboboxItem, { value: code, children: [_jsx("span", { className: "min-w-10 font-mono text-xs text-muted-foreground", children: code }), _jsx("span", { className: "truncate", children: currency?.name ?? code })] }, code));
45
+ } }) })] })] })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", onClick: () => !disabled && setEditing(true), disabled: disabled, className: cn("-mx-1 flex-1 truncate rounded px-1 py-0.5 text-left text-sm transition-colors", !disabled && "cursor-text hover:bg-muted/60", !value && "text-muted-foreground italic"), children: value ? (_jsxs("span", { children: [_jsx("span", { className: "font-mono", children: value }), currencyInfo ? (_jsx("span", { className: "ml-2 text-muted-foreground", children: currencyInfo.name })) : null] })) : (formatMessage(messages.addTemplate, { label: label.toLowerCase() })) }), !disabled ? (_jsx(Pencil, { className: "h-3 w-3 text-muted-foreground opacity-0 group-hover:opacity-100" })) : null] })), error ? _jsx("p", { className: "mt-1 text-xs text-destructive", children: error }) : null] })] }));
46
+ }
@@ -0,0 +1,16 @@
1
+ import { type ComponentType } from "react";
2
+ export type InlineFieldKind = "text" | "email" | "url" | "textarea";
3
+ export interface InlineFieldProps {
4
+ icon?: ComponentType<{
5
+ className?: string;
6
+ }>;
7
+ label: string;
8
+ value: string | null;
9
+ kind?: InlineFieldKind;
10
+ href?: string;
11
+ placeholder?: string;
12
+ disabled?: boolean;
13
+ onSave: (next: string | null) => Promise<void>;
14
+ }
15
+ export declare function InlineField({ icon: Icon, label, value, kind, href, placeholder, disabled, onSave, }: InlineFieldProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=inline-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-field.d.ts","sourceRoot":"","sources":["../../src/components/inline-field.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,aAAa,EAAgC,MAAM,OAAO,CAAA;AAIxE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,UAAU,CAAA;AAEnE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,eAAe,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/C;AAED,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EAAE,IAAI,EACV,KAAK,EACL,KAAK,EACL,IAAa,EACb,IAAI,EACJ,WAAW,EACX,QAAQ,EACR,MAAM,GACP,EAAE,gBAAgB,2CA+HlB"}
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button, cn, Input, Textarea } from "@voyant-travel/ui/components";
4
+ import { Check, Loader2, Pencil, X } from "lucide-react";
5
+ import { useState } from "react";
6
+ import { useCrmUiMessagesOrDefault } from "../i18n/index.js";
7
+ export function InlineField({ icon: Icon, label, value, kind = "text", href, placeholder, disabled, onSave, }) {
8
+ const messages = useCrmUiMessagesOrDefault().inlineEditor;
9
+ const [editing, setEditing] = useState(false);
10
+ const [draft, setDraft] = useState(value ?? "");
11
+ const [saving, setSaving] = useState(false);
12
+ const [error, setError] = useState(null);
13
+ function handleCancel() {
14
+ setDraft(value ?? "");
15
+ setEditing(false);
16
+ setError(null);
17
+ }
18
+ async function handleSave() {
19
+ setSaving(true);
20
+ setError(null);
21
+ try {
22
+ const trimmed = draft.trim();
23
+ await onSave(trimmed === "" ? null : trimmed);
24
+ setEditing(false);
25
+ }
26
+ catch (err) {
27
+ setError(err instanceof Error ? err.message : messages.failedToSave);
28
+ }
29
+ finally {
30
+ setSaving(false);
31
+ }
32
+ }
33
+ function handleKeyDown(event) {
34
+ if (kind !== "textarea" && event.key === "Enter") {
35
+ event.preventDefault();
36
+ void handleSave();
37
+ }
38
+ else if (event.key === "Escape") {
39
+ event.preventDefault();
40
+ handleCancel();
41
+ }
42
+ }
43
+ const display = value || (_jsx("span", { className: "text-muted-foreground italic", children: placeholder || messages.notSet }));
44
+ return (_jsxs("div", { className: "group flex items-start gap-3 py-1.5", children: [Icon ? _jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), editing ? (_jsxs("div", { className: "mt-1 flex items-start gap-2", children: [kind === "textarea" ? (_jsx(Textarea, { autoFocus: true, value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: handleKeyDown, disabled: saving, className: "min-h-[80px] text-sm", placeholder: placeholder })) : (_jsx(Input, { autoFocus: true, type: kind === "email" ? "email" : kind === "url" ? "url" : "text", value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: handleKeyDown, disabled: saving, className: "h-8 text-sm", placeholder: placeholder })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => void handleSave(), disabled: saving, className: "h-8 w-8 p-0", children: saving ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": "true" })) : (_jsx(Check, { className: "h-3.5 w-3.5", "aria-hidden": "true" })) }), _jsx(Button, { size: "sm", variant: "ghost", onClick: handleCancel, disabled: saving, className: "h-8 w-8 p-0", children: _jsx(X, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) })] })] })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: cn("flex-1 text-sm", kind !== "textarea" && "truncate"), children: href && value ? (_jsx("a", { href: href, className: "text-primary hover:underline", target: href.startsWith("http") ? "_blank" : undefined, rel: href.startsWith("http") ? "noreferrer" : undefined, children: value })) : (display) }), !disabled ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setEditing(true), className: "h-6 w-6 p-0 opacity-0 group-hover:opacity-100", children: _jsx(Pencil, { className: "h-3 w-3", "aria-hidden": "true" }) })) : null] })), error ? _jsx("p", { className: "mt-1 text-xs text-destructive", children: error }) : null] })] }));
45
+ }
@@ -0,0 +1,12 @@
1
+ import { type ComponentType } from "react";
2
+ export interface InlineLanguageFieldProps {
3
+ icon?: ComponentType<{
4
+ className?: string;
5
+ }>;
6
+ label: string;
7
+ value: string | null;
8
+ disabled?: boolean;
9
+ onSave: (next: string | null) => Promise<void>;
10
+ }
11
+ export declare function InlineLanguageField({ icon: Icon, label, value, disabled, onSave, }: InlineLanguageFieldProps): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=inline-language-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-language-field.d.ts","sourceRoot":"","sources":["../../src/components/inline-language-field.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,aAAa,EAAY,MAAM,OAAO,CAAA;AAMpD,MAAM,WAAW,wBAAwB;IACvC,IAAI,CAAC,EAAE,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/C;AAED,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EAAE,IAAI,EACV,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAAM,GACP,EAAE,wBAAwB,2CAuG1B"}
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyant-travel/i18n";
4
+ import { cn } from "@voyant-travel/ui/components";
5
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyant-travel/ui/components/combobox";
6
+ import { languages } from "@voyant-travel/utils/languages";
7
+ import { Pencil } from "lucide-react";
8
+ import { useState } from "react";
9
+ import { useCrmUiMessagesOrDefault } from "../i18n/index.js";
10
+ const LANGUAGE_CODES = Object.keys(languages).sort();
11
+ export function InlineLanguageField({ icon: Icon, label, value, disabled, onSave, }) {
12
+ const messages = useCrmUiMessagesOrDefault().inlineEditor;
13
+ const [editing, setEditing] = useState(false);
14
+ const [saving, setSaving] = useState(false);
15
+ const [error, setError] = useState(null);
16
+ async function commitSelection(next) {
17
+ setSaving(true);
18
+ setError(null);
19
+ try {
20
+ await onSave(next);
21
+ setEditing(false);
22
+ }
23
+ catch (err) {
24
+ setError(err instanceof Error ? err.message : messages.failedToSave);
25
+ }
26
+ finally {
27
+ setSaving(false);
28
+ }
29
+ }
30
+ const languageName = value ? languages[value] : null;
31
+ return (_jsxs("div", { className: "group flex items-start gap-3 py-1.5", children: [Icon ? _jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), editing ? (_jsxs(Combobox, { items: LANGUAGE_CODES, defaultOpen: true, autoHighlight: true, filter: (code, query) => {
32
+ const languageCode = String(code);
33
+ const languageName = languages[languageCode];
34
+ if (!languageName)
35
+ return false;
36
+ const normalized = query.toLowerCase();
37
+ return (languageCode.toLowerCase().includes(normalized) ||
38
+ languageName.toLowerCase().includes(normalized));
39
+ }, onValueChange: (next) => void commitSelection(next ?? null), onOpenChange: (open) => {
40
+ if (!open)
41
+ setEditing(false);
42
+ }, children: [_jsx(ComboboxInput, { autoFocus: true, placeholder: messages.searchLanguagePlaceholder, className: "mt-0.5 h-8 text-sm", disabled: saving }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: messages.noLanguagesFound }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (code) => {
43
+ const languageName = languages[code];
44
+ return (_jsxs(ComboboxItem, { value: code, children: [_jsx("span", { className: "min-w-10 font-mono text-xs text-muted-foreground", children: code }), _jsx("span", { className: "truncate", children: languageName })] }, code));
45
+ } }) })] })] })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", onClick: () => !disabled && setEditing(true), disabled: disabled, className: cn("-mx-1 flex-1 truncate rounded px-1 py-0.5 text-left text-sm transition-colors", !disabled && "cursor-text hover:bg-muted/60", !value && "text-muted-foreground italic"), children: value ? (_jsxs("span", { children: [_jsx("span", { className: "font-mono", children: value }), languageName ? (_jsx("span", { className: "ml-2 text-muted-foreground", children: languageName })) : null] })) : (formatMessage(messages.addTemplate, { label: label.toLowerCase() })) }), !disabled ? (_jsx(Pencil, { className: "h-3 w-3 text-muted-foreground opacity-0 group-hover:opacity-100" })) : null] })), error ? _jsx("p", { className: "mt-1 text-xs text-destructive", children: error }) : null] })] }));
46
+ }
@@ -0,0 +1,15 @@
1
+ import { type ComponentType } from "react";
2
+ export interface InlineNumberFieldProps {
3
+ icon?: ComponentType<{
4
+ className?: string;
5
+ }>;
6
+ label: string;
7
+ value: number | null;
8
+ placeholder?: string;
9
+ disabled?: boolean;
10
+ min?: number;
11
+ max?: number;
12
+ onSave: (next: number | null) => Promise<void>;
13
+ }
14
+ export declare function InlineNumberField({ icon: Icon, label, value, placeholder, disabled, min, max, onSave, }: InlineNumberFieldProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=inline-number-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-number-field.d.ts","sourceRoot":"","sources":["../../src/components/inline-number-field.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,aAAa,EAAgC,MAAM,OAAO,CAAA;AAIxE,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/C;AAED,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EAAE,IAAI,EACV,KAAK,EACL,KAAK,EACL,WAAW,EACX,QAAQ,EACR,GAAG,EACH,GAAG,EACH,MAAM,GACP,EAAE,sBAAsB,2CAqHxB"}
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyant-travel/i18n";
4
+ import { Button, Input } from "@voyant-travel/ui/components";
5
+ import { Check, Loader2, Pencil, X } from "lucide-react";
6
+ import { useState } from "react";
7
+ import { useCrmUiMessagesOrDefault } from "../i18n/index.js";
8
+ export function InlineNumberField({ icon: Icon, label, value, placeholder, disabled, min, max, onSave, }) {
9
+ const messages = useCrmUiMessagesOrDefault().inlineEditor;
10
+ const [editing, setEditing] = useState(false);
11
+ const [draft, setDraft] = useState(value != null ? String(value) : "");
12
+ const [saving, setSaving] = useState(false);
13
+ const [error, setError] = useState(null);
14
+ function handleCancel() {
15
+ setDraft(value != null ? String(value) : "");
16
+ setEditing(false);
17
+ setError(null);
18
+ }
19
+ async function handleSave() {
20
+ setSaving(true);
21
+ setError(null);
22
+ try {
23
+ if (draft.trim() === "") {
24
+ await onSave(null);
25
+ }
26
+ else {
27
+ const parsed = Number.parseInt(draft, 10);
28
+ if (!Number.isFinite(parsed))
29
+ throw new Error(messages.invalidNumber);
30
+ if (min != null && parsed < min) {
31
+ throw new Error(formatMessage(messages.minNumber, { min: String(min) }));
32
+ }
33
+ if (max != null && parsed > max) {
34
+ throw new Error(formatMessage(messages.maxNumber, { max: String(max) }));
35
+ }
36
+ await onSave(parsed);
37
+ }
38
+ setEditing(false);
39
+ }
40
+ catch (err) {
41
+ setError(err instanceof Error ? err.message : messages.failedToSave);
42
+ }
43
+ finally {
44
+ setSaving(false);
45
+ }
46
+ }
47
+ function handleKeyDown(event) {
48
+ if (event.key === "Enter") {
49
+ event.preventDefault();
50
+ void handleSave();
51
+ }
52
+ else if (event.key === "Escape") {
53
+ event.preventDefault();
54
+ handleCancel();
55
+ }
56
+ }
57
+ return (_jsxs("div", { className: "group flex items-start gap-3 py-1.5", children: [Icon ? _jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), editing ? (_jsxs("div", { className: "mt-1 flex items-center gap-2", children: [_jsx(Input, { autoFocus: true, type: "number", value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: handleKeyDown, disabled: saving, className: "h-8 text-sm", placeholder: placeholder, min: min, max: max }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => void handleSave(), disabled: saving, className: "h-8 w-8 p-0", children: saving ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": "true" })) : (_jsx(Check, { className: "h-3.5 w-3.5", "aria-hidden": "true" })) }), _jsx(Button, { size: "sm", variant: "ghost", onClick: handleCancel, disabled: saving, className: "h-8 w-8 p-0", children: _jsx(X, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) })] })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex-1 truncate text-sm", children: value != null ? (value.toLocaleString()) : (_jsx("span", { className: "text-muted-foreground italic", children: placeholder || messages.notSet })) }), !disabled ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setEditing(true), className: "h-6 w-6 p-0 opacity-0 group-hover:opacity-100", children: _jsx(Pencil, { className: "h-3 w-3", "aria-hidden": "true" }) })) : null] })), error ? _jsx("p", { className: "mt-1 text-xs text-destructive", children: error }) : null] })] }));
58
+ }
@@ -0,0 +1,19 @@
1
+ import { type ComponentType } from "react";
2
+ export interface InlineSelectFieldOption {
3
+ value: string;
4
+ label: string;
5
+ }
6
+ export interface InlineSelectFieldProps {
7
+ icon?: ComponentType<{
8
+ className?: string;
9
+ }>;
10
+ label: string;
11
+ value: string | null;
12
+ options: readonly InlineSelectFieldOption[];
13
+ placeholder?: string;
14
+ disabled?: boolean;
15
+ allowClear?: boolean;
16
+ onSave: (next: string | null) => Promise<void>;
17
+ }
18
+ export declare function InlineSelectField({ icon: Icon, label, value, options, placeholder, disabled, allowClear, onSave, }: InlineSelectFieldProps): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=inline-select-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-select-field.d.ts","sourceRoot":"","sources":["../../src/components/inline-select-field.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,aAAa,EAAY,MAAM,OAAO,CAAA;AAIpD,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAA;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/C;AAED,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EAAE,IAAI,EACV,KAAK,EACL,KAAK,EACL,OAAO,EACP,WAAW,EACX,QAAQ,EACR,UAAiB,EACjB,MAAM,GACP,EAAE,sBAAsB,2CAsGxB"}
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyant-travel/ui/components";
4
+ import { Check, Loader2, Pencil, X } from "lucide-react";
5
+ import { useState } from "react";
6
+ import { useCrmUiMessagesOrDefault } from "../i18n/index.js";
7
+ export function InlineSelectField({ icon: Icon, label, value, options, placeholder, disabled, allowClear = true, onSave, }) {
8
+ const messages = useCrmUiMessagesOrDefault().inlineEditor;
9
+ const [editing, setEditing] = useState(false);
10
+ const [draft, setDraft] = useState(value ?? "");
11
+ const [saving, setSaving] = useState(false);
12
+ const [error, setError] = useState(null);
13
+ function handleCancel() {
14
+ setDraft(value ?? "");
15
+ setEditing(false);
16
+ setError(null);
17
+ }
18
+ async function handleSave() {
19
+ setSaving(true);
20
+ setError(null);
21
+ try {
22
+ await onSave(draft === "__none__" || draft === "" ? null : draft);
23
+ setEditing(false);
24
+ }
25
+ catch (err) {
26
+ setError(err instanceof Error ? err.message : messages.failedToSave);
27
+ }
28
+ finally {
29
+ setSaving(false);
30
+ }
31
+ }
32
+ const matched = options.find((option) => option.value === value);
33
+ return (_jsxs("div", { className: "group flex items-start gap-3 py-1.5", children: [Icon ? _jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), editing ? (_jsxs("div", { className: "mt-1 flex items-center gap-2", children: [_jsxs(Select, { value: draft, onValueChange: (next) => setDraft(next ?? ""), disabled: saving, children: [_jsx(SelectTrigger, { className: "h-8 flex-1 text-sm", children: _jsx(SelectValue, { placeholder: placeholder || messages.selectPlaceholder }) }), _jsxs(SelectContent, { children: [allowClear ? (_jsx(SelectItem, { value: "__none__", children: _jsx("span", { className: "text-muted-foreground italic", children: messages.noneOption }) })) : null, options.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value)))] })] }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => void handleSave(), disabled: saving, className: "h-8 w-8 p-0", children: saving ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": "true" })) : (_jsx(Check, { className: "h-3.5 w-3.5", "aria-hidden": "true" })) }), _jsx(Button, { size: "sm", variant: "ghost", onClick: handleCancel, disabled: saving, className: "h-8 w-8 p-0", children: _jsx(X, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) })] })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex-1 truncate text-sm", children: matched ? (matched.label) : (_jsx("span", { className: "text-muted-foreground italic", children: placeholder || messages.notSet })) }), !disabled ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setEditing(true), className: "h-6 w-6 p-0 opacity-0 group-hover:opacity-100", children: _jsx(Pencil, { className: "h-3 w-3", "aria-hidden": "true" }) })) : null] })), error ? _jsx("p", { className: "mt-1 text-xs text-destructive", children: error }) : null] })] }));
34
+ }
@@ -0,0 +1,11 @@
1
+ export interface QuoteSummaryCardProps {
2
+ title: string;
3
+ pipelineName?: string | null;
4
+ stageName?: string | null;
5
+ status: string;
6
+ valueAmountCents?: number | null;
7
+ valueCurrency?: string | null;
8
+ expectedCloseDate?: string | null;
9
+ }
10
+ export declare function QuoteSummaryCard({ title, pipelineName, stageName, status, valueAmountCents, valueCurrency, expectedCloseDate, }: QuoteSummaryCardProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=quote-summary-card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-summary-card.d.ts","sourceRoot":"","sources":["../../src/components/quote-summary-card.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,YAAY,EACZ,SAAS,EACT,MAAM,EACN,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAClB,EAAE,qBAAqB,2CAgCvB"}
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Badge, Card, CardContent } from "@voyant-travel/ui/components";
3
+ import { TrendingUp } from "lucide-react";
4
+ import { useCrmUiI18nOrDefault } from "../i18n/index.js";
5
+ import { formatCrmDate, formatCrmMoney } from "./crm-format.js";
6
+ export function QuoteSummaryCard({ title, pipelineName, stageName, status, valueAmountCents, valueCurrency, expectedCloseDate, }) {
7
+ const i18n = useCrmUiI18nOrDefault();
8
+ const { messages } = i18n;
9
+ const statusLabel = messages.common.quoteStatusLabels[status] ?? status;
10
+ return (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h2", { className: "text-lg font-semibold leading-tight", children: title }), _jsxs("p", { className: "mt-1 text-sm text-muted-foreground", children: [pipelineName ?? messages.quoteSummaryCard.unknown, " -", " ", stageName ?? messages.quoteSummaryCard.unknown] })] }), _jsx(Badge, { variant: "outline", children: statusLabel })] }), _jsxs("div", { className: "mt-3 flex items-center gap-2", children: [_jsx(TrendingUp, { className: "size-4 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "text-lg font-semibold", children: formatCrmMoney(i18n, valueAmountCents, valueCurrency) })] }), expectedCloseDate ? (_jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [messages.quoteSummaryCard.expectedClose, ": ", formatCrmDate(i18n, expectedCloseDate)] })) : null] }) }));
11
+ }
@@ -0,0 +1,22 @@
1
+ import type { QuoteVersionLineRecord } from "../index.js";
2
+ export interface QuoteVersionLinesCardProps {
3
+ currency: string;
4
+ lines: QuoteVersionLineRecord[];
5
+ isLoading: boolean;
6
+ onCreate: (input: {
7
+ description: string;
8
+ currency: string;
9
+ quantity: number;
10
+ unitPriceAmountCents: number;
11
+ totalAmountCents: number;
12
+ }) => Promise<void>;
13
+ onUpdate: (lineId: string, input: Partial<{
14
+ description: string;
15
+ quantity: number;
16
+ unitPriceAmountCents: number;
17
+ totalAmountCents: number;
18
+ }>) => Promise<void>;
19
+ onRemove: (lineId: string) => Promise<void>;
20
+ }
21
+ export declare function QuoteVersionLinesCard({ currency, lines, isLoading, onCreate, onUpdate, onRemove, }: QuoteVersionLinesCardProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=quote-version-detail-sections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-version-detail-sections.d.ts","sourceRoot":"","sources":["../../src/components/quote-version-detail-sections.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGzD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,sBAAsB,EAAE,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,CAAC,KAAK,EAAE;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,QAAQ,EAAE,MAAM,CAAA;QAChB,QAAQ,EAAE,MAAM,CAAA;QAChB,oBAAoB,EAAE,MAAM,CAAA;QAC5B,gBAAgB,EAAE,MAAM,CAAA;KACzB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnB,QAAQ,EAAE,CACR,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,CAAC;QACb,WAAW,EAAE,MAAM,CAAA;QACnB,QAAQ,EAAE,MAAM,CAAA;QAChB,oBAAoB,EAAE,MAAM,CAAA;QAC5B,gBAAgB,EAAE,MAAM,CAAA;KACzB,CAAC,KACC,OAAO,CAAC,IAAI,CAAC,CAAA;IAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5C;AAED,wBAAgB,qBAAqB,CAAC,EACpC,QAAQ,EACR,KAAK,EACL,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,0BAA0B,2CAuH5B"}
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, } from "@voyant-travel/ui/components";
3
+ import { CurrencyInput } from "@voyant-travel/ui/components/currency-input";
4
+ import { Loader2, Plus, Trash2 } from "lucide-react";
5
+ import { useEffect, useState } from "react";
6
+ import { useCrmUiI18nOrDefault } from "../i18n/index.js";
7
+ import { formatCrmMoney } from "./crm-format.js";
8
+ export function QuoteVersionLinesCard({ currency, lines, isLoading, onCreate, onUpdate, onRemove, }) {
9
+ const i18n = useCrmUiI18nOrDefault();
10
+ const { messages } = i18n;
11
+ const [newDescription, setNewDescription] = useState("");
12
+ const [newQuantity, setNewQuantity] = useState("1");
13
+ const [newPriceCents, setNewPriceCents] = useState(0);
14
+ const [adding, setAdding] = useState(false);
15
+ const [error, setError] = useState(null);
16
+ async function handleAdd() {
17
+ const description = newDescription.trim();
18
+ if (!description) {
19
+ setError(messages.quoteVersionLinesCard.validation.descriptionRequired);
20
+ return;
21
+ }
22
+ const quantity = Number.parseInt(newQuantity, 10) || 1;
23
+ const price = newPriceCents ?? 0;
24
+ setAdding(true);
25
+ setError(null);
26
+ try {
27
+ await onCreate({
28
+ description,
29
+ currency,
30
+ quantity,
31
+ unitPriceAmountCents: price,
32
+ totalAmountCents: quantity * price,
33
+ });
34
+ setNewDescription("");
35
+ setNewQuantity("1");
36
+ setNewPriceCents(0);
37
+ }
38
+ catch (err) {
39
+ setError(err instanceof Error ? err.message : messages.quoteVersionLinesCard.validation.addFailed);
40
+ }
41
+ finally {
42
+ setAdding(false);
43
+ }
44
+ }
45
+ const subtotal = lines.reduce((sum, line) => sum + line.totalAmountCents, 0);
46
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "text-sm font-semibold", children: messages.quoteVersionLinesCard.title }) }), _jsxs(CardContent, { children: [isLoading ? (_jsx("div", { className: "flex justify-center py-6", children: _jsx(Loader2, { className: "size-5 animate-spin text-muted-foreground" }) })) : lines.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: messages.quoteVersionLinesCard.empty })) : (_jsx("ul", { className: "divide-y", children: lines.map((line) => (_jsx(QuoteVersionLineRow, { currency: currency, line: line, onUpdate: (input) => onUpdate(line.id, input), onRemove: () => onRemove(line.id) }, line.id))) })), _jsxs("div", { className: "mt-3 flex flex-col gap-2 border-t pt-3", children: [_jsxs("div", { className: "grid grid-cols-12 gap-2", children: [_jsx(Input, { className: "col-span-6 h-8 text-sm", value: newDescription, onChange: (event) => setNewDescription(event.target.value), placeholder: messages.quoteVersionLinesCard.fields.description }), _jsx(Input, { className: "col-span-2 h-8 text-sm", type: "number", min: 1, value: newQuantity, onChange: (event) => setNewQuantity(event.target.value), placeholder: messages.quoteVersionLinesCard.fields.quantity }), _jsx(CurrencyInput, { className: "col-span-3 h-8 text-sm", inputClassName: "h-8 text-sm", value: newPriceCents, onChange: setNewPriceCents, currency: currency, placeholder: messages.quoteVersionLinesCard.fields.priceCents }), _jsx(Button, { size: "sm", className: "col-span-1 h-8", onClick: () => void handleAdd(), disabled: adding, "aria-label": messages.common.create, children: adding ? (_jsx(Loader2, { className: "size-3.5 animate-spin", "aria-hidden": "true" })) : (_jsx(Plus, { className: "size-3.5", "aria-hidden": "true" })) })] }), error ? _jsx("p", { className: "text-xs text-destructive", children: error }) : null] }), _jsxs("div", { className: "mt-3 flex items-center justify-between border-t pt-3 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: messages.quoteVersionLinesCard.subtotal }), _jsx("span", { className: "font-semibold", children: formatCrmMoney(i18n, subtotal, currency) })] })] })] }));
47
+ }
48
+ function QuoteVersionLineRow({ currency, line, onUpdate, onRemove, }) {
49
+ const i18n = useCrmUiI18nOrDefault();
50
+ const [removing, setRemoving] = useState(false);
51
+ const [draftPriceCents, setDraftPriceCents] = useState(line.unitPriceAmountCents);
52
+ useEffect(() => {
53
+ setDraftPriceCents(line.unitPriceAmountCents);
54
+ }, [line.unitPriceAmountCents]);
55
+ async function handleRemove() {
56
+ setRemoving(true);
57
+ try {
58
+ await onRemove();
59
+ }
60
+ finally {
61
+ setRemoving(false);
62
+ }
63
+ }
64
+ async function handleQuantity(value) {
65
+ const quantity = Number.parseInt(value, 10);
66
+ if (!Number.isFinite(quantity) || quantity < 1)
67
+ return;
68
+ await onUpdate({
69
+ quantity,
70
+ totalAmountCents: quantity * line.unitPriceAmountCents,
71
+ });
72
+ }
73
+ async function handlePrice(value) {
74
+ if (value == null || value < 0 || value === line.unitPriceAmountCents)
75
+ return;
76
+ await onUpdate({
77
+ unitPriceAmountCents: value,
78
+ totalAmountCents: line.quantity * value,
79
+ });
80
+ }
81
+ return (_jsx("li", { className: "py-2", children: _jsxs("div", { className: "grid grid-cols-12 items-center gap-2", children: [_jsx(Input, { className: "col-span-6 h-8 text-sm", defaultValue: line.description, onBlur: (event) => {
82
+ const value = event.target.value.trim();
83
+ if (value && value !== line.description) {
84
+ void onUpdate({ description: value });
85
+ }
86
+ } }), _jsx(Input, { className: "col-span-2 h-8 text-sm", type: "number", min: 1, defaultValue: line.quantity, onBlur: (event) => void handleQuantity(event.target.value) }), _jsx(CurrencyInput, { className: "col-span-2 h-8 text-sm", inputClassName: "h-8 text-sm", value: draftPriceCents, onChange: setDraftPriceCents, onBlur: () => void handlePrice(draftPriceCents), currency: currency }), _jsx("span", { className: "col-span-1 text-right text-sm font-medium", children: formatCrmMoney(i18n, line.totalAmountCents, currency) }), _jsx(Button, { size: "sm", variant: "ghost", className: "col-span-1 size-8 p-0", onClick: () => void handleRemove(), disabled: removing, children: removing ? (_jsx(Loader2, { className: "size-3.5 animate-spin", "aria-hidden": "true" })) : (_jsx(Trash2, { className: "size-3.5", "aria-hidden": "true" })) })] }) }));
87
+ }
@@ -0,0 +1,8 @@
1
+ import { type QuoteVersionRecord } from "../index.js";
2
+ export interface QuoteVersionsPageProps {
3
+ onQuoteVersionOpen?: (quoteVersion: QuoteVersionRecord) => void;
4
+ onQuoteVersionCreated?: (quoteVersion: QuoteVersionRecord) => void;
5
+ className?: string;
6
+ }
7
+ export declare function QuoteVersionsPage({ onQuoteVersionOpen, onQuoteVersionCreated, className, }?: QuoteVersionsPageProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=quote-versions-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-versions-page.d.ts","sourceRoot":"","sources":["../../src/components/quote-versions-page.tsx"],"names":[],"mappings":"AAsBA,OAAO,EAAE,KAAK,kBAAkB,EAAoB,MAAM,aAAa,CAAA;AAIvE,MAAM,WAAW,sBAAsB;IACrC,kBAAkB,CAAC,EAAE,CAAC,YAAY,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC/D,qBAAqB,CAAC,EAAE,CAAC,YAAY,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,kBAAkB,EAClB,qBAAqB,EACrB,SAAS,GACV,GAAE,sBAA2B,2CAwH7B"}
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Badge, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyant-travel/ui/components";
3
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyant-travel/ui/components/table";
4
+ import { cn } from "@voyant-travel/ui/lib/utils";
5
+ import { Loader2, Plus } from "lucide-react";
6
+ import { useState } from "react";
7
+ import { useCrmUiI18nOrDefault } from "../i18n/index.js";
8
+ import { useQuoteVersions } from "../index.js";
9
+ import { CreateQuoteVersionDialog } from "./create-quote-version-dialog.js";
10
+ import { formatCrmDate, formatCrmMoney, formatCrmRelative } from "./crm-format.js";
11
+ export function QuoteVersionsPage({ onQuoteVersionOpen, onQuoteVersionCreated, className, } = {}) {
12
+ const i18n = useCrmUiI18nOrDefault();
13
+ const { messages } = i18n;
14
+ const [statusFilter, setStatusFilter] = useState("all");
15
+ const [dialogOpen, setDialogOpen] = useState(false);
16
+ const { data, isPending, isError } = useQuoteVersions({
17
+ status: statusFilter === "all" ? undefined : statusFilter,
18
+ limit: 100,
19
+ });
20
+ const quoteVersions = data?.data ?? [];
21
+ const quoteVersionStatusOptions = Object.entries(messages.common.quoteVersionStatusLabels).map(([value, label]) => ({ value, label }));
22
+ const handleCreated = (quoteVersion) => {
23
+ onQuoteVersionCreated?.(quoteVersion);
24
+ onQuoteVersionOpen?.(quoteVersion);
25
+ };
26
+ return (_jsxs("div", { "data-slot": "quote-versions-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: messages.quoteVersionsPage.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: messages.quoteVersionsPage.description })] }), _jsxs(Button, { onClick: () => setDialogOpen(true), children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.quoteVersionsPage.create] })] }), _jsx("div", { className: "flex flex-wrap items-center gap-3", children: _jsxs(Select, { value: statusFilter, onValueChange: (value) => setStatusFilter(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[180px] text-sm", children: _jsx(SelectValue, { placeholder: messages.quoteVersionsPage.filters.status }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: messages.quoteVersionsPage.filters.allStatuses }), quoteVersionStatusOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value)))] })] }) }), _jsx("div", { className: "overflow-hidden rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: messages.quoteVersionsPage.columns.quoteVersion }), _jsx(TableHead, { children: messages.quoteVersionsPage.columns.status }), _jsx(TableHead, { children: messages.quoteVersionsPage.columns.total }), _jsx(TableHead, { children: messages.quoteVersionsPage.columns.validUntil }), _jsx(TableHead, { children: messages.quoteVersionsPage.columns.updated })] }) }), _jsx(TableBody, { children: isPending ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center", children: _jsx(Loader2, { className: "mx-auto size-4 animate-spin text-muted-foreground" }) }) })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center text-sm text-destructive", children: messages.quoteVersionsPage.loadFailed }) })) : quoteVersions.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center text-sm text-muted-foreground", children: messages.quoteVersionsPage.empty }) })) : (quoteVersions.map((quoteVersion) => {
27
+ const statusLabel = messages.common.quoteVersionStatusLabels[quoteVersion.status] ?? quoteVersion.status;
28
+ return (_jsxs(TableRow, { onClick: () => onQuoteVersionOpen?.(quoteVersion), className: cn(onQuoteVersionOpen && "cursor-pointer"), children: [_jsx(TableCell, { className: "font-mono text-xs", children: quoteVersion.label ?? quoteVersion.id.slice(-8) }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: statusLabel }) }), _jsx(TableCell, { className: "font-medium", children: formatCrmMoney(i18n, quoteVersion.totalAmountCents, quoteVersion.currency) }), _jsx(TableCell, { children: formatCrmDate(i18n, quoteVersion.validUntil) }), _jsx(TableCell, { className: "text-muted-foreground", children: formatCrmRelative(i18n, quoteVersion.updatedAt) })] }, quoteVersion.id));
29
+ })) })] }) }), _jsx(CreateQuoteVersionDialog, { open: dialogOpen, onOpenChange: setDialogOpen, onCreated: handleCreated })] }));
30
+ }
@@ -0,0 +1,8 @@
1
+ import type { QuoteRecord as QuoteData, StageRecord as StageData } from "../index.js";
2
+ export interface QuotesBoardProps {
3
+ stages: StageData[];
4
+ quotesByStage: Map<string, QuoteData[]>;
5
+ onQuoteOpen?: (quote: QuoteData) => void;
6
+ }
7
+ export declare function QuotesBoard({ stages, quotesByStage, onQuoteOpen }: QuotesBoardProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=quotes-board.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quotes-board.d.ts","sourceRoot":"","sources":["../../src/components/quotes-board.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,IAAI,SAAS,EAAE,WAAW,IAAI,SAAS,EAAE,MAAM,aAAa,CAAA;AAGrF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;IACvC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAA;CACzC;AAED,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,EAAE,gBAAgB,2CA6CnF"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Card } from "@voyant-travel/ui/components";
3
+ import { ScrollArea, ScrollBar } from "@voyant-travel/ui/components/scroll-area";
4
+ import { TrendingUp } from "lucide-react";
5
+ import { useCrmUiI18nOrDefault } from "../i18n/index.js";
6
+ import { formatCrmDate, formatCrmMoney } from "./crm-format.js";
7
+ export function QuotesBoard({ stages, quotesByStage, onQuoteOpen }) {
8
+ const i18n = useCrmUiI18nOrDefault();
9
+ const { messages } = i18n;
10
+ return (_jsxs(ScrollArea, { className: "flex-1", children: [_jsx("div", { className: "flex gap-3 pb-2", children: stages.map((stage) => {
11
+ const quotes = quotesByStage.get(stage.id) ?? [];
12
+ const total = quotes.reduce((sum, quote) => sum + (quote.valueAmountCents ?? 0), 0);
13
+ const primaryCurrency = quotes[0]?.valueCurrency ?? null;
14
+ return (_jsxs("div", { className: "flex w-[280px] min-w-[280px] flex-col gap-2 rounded-md border bg-muted/30 p-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-2 py-1", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: stage.name || messages.quotesBoard.fallbackName }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [i18n.formatNumber(quotes.length), " -", " ", formatCrmMoney(i18n, total, primaryCurrency)] })] }), stage.probability != null ? (_jsxs("span", { className: "rounded border px-1.5 py-0.5 text-[10px]", children: [i18n.formatNumber(stage.probability), "%"] })) : null] }), _jsx("div", { className: "flex flex-col gap-2", children: quotes.map((quote) => (_jsx(QuoteBoardCard, { quote: quote, onOpen: onQuoteOpen }, quote.id))) })] }, stage.id));
15
+ }) }), _jsx(ScrollBar, { orientation: "horizontal" })] }));
16
+ }
17
+ function QuoteBoardCard({ quote, onOpen, }) {
18
+ const i18n = useCrmUiI18nOrDefault();
19
+ const content = (_jsxs(_Fragment, { children: [_jsx("p", { className: "line-clamp-2 font-medium", children: quote.title }), _jsxs("div", { className: "mt-2 flex items-center justify-between gap-2", children: [_jsxs("span", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(TrendingUp, { className: "size-3", "aria-hidden": "true" }), formatCrmMoney(i18n, quote.valueAmountCents, quote.valueCurrency)] }), quote.expectedCloseDate ? (_jsx("span", { className: "text-xs text-muted-foreground", children: formatCrmDate(i18n, quote.expectedCloseDate) })) : null] })] }));
20
+ if (onOpen) {
21
+ return (_jsx(Card, { className: "p-3 text-sm", children: _jsx("button", { type: "button", className: "block w-full text-left", onClick: () => onOpen(quote), children: content }) }));
22
+ }
23
+ return _jsx(Card, { className: "p-3 text-sm", children: content });
24
+ }
@@ -0,0 +1,10 @@
1
+ export { type CreatePipelineInput, type CreateStageInput, type UpdatePipelineInput, type UpdateStageInput, usePipelineMutation, } from "./use-pipeline-mutation.js";
2
+ export { type UsePipelineOptions, type UsePipelinesOptions, usePipeline, usePipelines, } from "./use-pipelines.js";
3
+ export { type UseQuoteOptions, useQuote } from "./use-quote.js";
4
+ export { type CreateQuoteInput, type UpdateQuoteInput, useQuoteMutation, } from "./use-quote-mutation.js";
5
+ export { type UseQuoteVersionOptions, useQuoteVersion, useQuoteVersionLines, } from "./use-quote-version.js";
6
+ export { type CreateQuoteVersionInput, type CreateQuoteVersionLineInput, type ExpireQuoteVersionsInput, type SendQuoteVersionInput, type UpdateQuoteVersionInput, type UpdateQuoteVersionLineInput, useQuoteVersionMutation, } from "./use-quote-version-mutation.js";
7
+ export { type UseQuoteVersionsOptions, useQuoteVersions, } from "./use-quote-versions.js";
8
+ export { type UseQuotesOptions, useQuotes } from "./use-quotes.js";
9
+ export { type UseStageOptions, type UseStagesOptions, useStage, useStages, } from "./use-stages.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,mBAAmB,GACpB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,WAAW,EACX,YAAY,GACb,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC/D,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,gBAAgB,GACjB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,KAAK,sBAAsB,EAC3B,eAAe,EACf,oBAAoB,GACrB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AACxC,OAAO,EACL,KAAK,uBAAuB,EAC5B,gBAAgB,GACjB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,QAAQ,EACR,SAAS,GACV,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,9 @@
1
+ export { usePipelineMutation, } from "./use-pipeline-mutation.js";
2
+ export { usePipeline, usePipelines, } from "./use-pipelines.js";
3
+ export { useQuote } from "./use-quote.js";
4
+ export { useQuoteMutation, } from "./use-quote-mutation.js";
5
+ export { useQuoteVersion, useQuoteVersionLines, } from "./use-quote-version.js";
6
+ export { useQuoteVersionMutation, } from "./use-quote-version-mutation.js";
7
+ export { useQuoteVersions, } from "./use-quote-versions.js";
8
+ export { useQuotes } from "./use-quotes.js";
9
+ export { useStage, useStages, } from "./use-stages.js";