@voyant-travel/quotes-react 0.121.0 → 0.123.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/index.d.ts +51 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +85 -0
- package/dist/admin/pages/quote-detail-page.d.ts +13 -0
- package/dist/admin/pages/quote-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/quote-detail-page.js +397 -0
- package/dist/admin/pipeline-dialogs.d.ts +16 -0
- package/dist/admin/pipeline-dialogs.d.ts.map +1 -0
- package/dist/admin/pipeline-dialogs.js +105 -0
- package/dist/admin/quote-content-sections.d.ts +57 -0
- package/dist/admin/quote-content-sections.d.ts.map +1 -0
- package/dist/admin/quote-content-sections.js +181 -0
- package/dist/admin/quote-detail-sections.d.ts +34 -0
- package/dist/admin/quote-detail-sections.d.ts.map +1 -0
- package/dist/admin/quote-detail-sections.js +52 -0
- package/dist/admin/quotes-board-host.d.ts +10 -0
- package/dist/admin/quotes-board-host.d.ts.map +1 -0
- package/dist/admin/quotes-board-host.js +72 -0
- package/dist/hooks/use-quote-media-mutation.d.ts +51 -0
- package/dist/hooks/use-quote-media-mutation.d.ts.map +1 -0
- package/dist/hooks/use-quote-media-mutation.js +63 -0
- package/dist/hooks/use-quote-media.d.ts +21 -0
- package/dist/hooks/use-quote-media.d.ts.map +1 -0
- package/dist/hooks/use-quote-media.js +19 -0
- package/dist/hooks/use-quote-mutation.d.ts +8 -0
- package/dist/hooks/use-quote-mutation.d.ts.map +1 -1
- package/dist/hooks/use-quote-participant-mutation.d.ts +24 -0
- package/dist/hooks/use-quote-participant-mutation.d.ts.map +1 -0
- package/dist/hooks/use-quote-participant-mutation.js +28 -0
- package/dist/hooks/use-quote-participants.d.ts +15 -0
- package/dist/hooks/use-quote-participants.d.ts.map +1 -0
- package/dist/hooks/use-quote-participants.js +16 -0
- package/dist/hooks/use-quote-product-mutation.d.ts +57 -0
- package/dist/hooks/use-quote-product-mutation.d.ts.map +1 -0
- package/dist/hooks/use-quote-product-mutation.js +36 -0
- package/dist/hooks/use-quote-products.d.ts +22 -0
- package/dist/hooks/use-quote-products.d.ts.map +1 -0
- package/dist/hooks/use-quote-products.js +19 -0
- package/dist/hooks/use-quote-version-mutation.d.ts +79 -0
- package/dist/hooks/use-quote-version-mutation.d.ts.map +1 -1
- package/dist/hooks/use-quote-version-mutation.js +58 -0
- package/dist/hooks/use-quote.d.ts +4 -0
- package/dist/hooks/use-quote.d.ts.map +1 -1
- package/dist/hooks/use-quotes.d.ts +4 -0
- package/dist/hooks/use-quotes.d.ts.map +1 -1
- package/dist/i18n/en/commerce.d.ts +145 -0
- package/dist/i18n/en/commerce.d.ts.map +1 -1
- package/dist/i18n/en/commerce.js +145 -0
- package/dist/i18n/en.d.ts +145 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/messages.d.ts +145 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +290 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro/commerce.d.ts +145 -0
- package/dist/i18n/ro/commerce.d.ts.map +1 -1
- package/dist/i18n/ro/commerce.js +145 -0
- package/dist/i18n/ro.d.ts +145 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/query-keys.d.ts +3 -0
- package/dist/query-keys.d.ts.map +1 -1
- package/dist/query-keys.js +3 -0
- package/dist/query-options.d.ts +32 -0
- package/dist/query-options.d.ts.map +1 -1
- package/dist/schemas.d.ts +142 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +47 -0
- package/package.json +27 -7
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button, Checkbox, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, } from "@voyant-travel/ui/components";
|
|
4
|
+
import { Loader2, Plus, Trash2 } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { useCrmUiMessagesOrDefault } from "../i18n/index.js";
|
|
7
|
+
import { usePipelineMutation } from "../index.js";
|
|
8
|
+
export function CreatePipelineDialog({ open, onOpenChange, existingCount, onCreated, }) {
|
|
9
|
+
const messages = useCrmUiMessagesOrDefault();
|
|
10
|
+
const t = messages.createPipelineDialog;
|
|
11
|
+
const { createPipeline } = usePipelineMutation();
|
|
12
|
+
const [name, setName] = useState("");
|
|
13
|
+
const [isDefault, setIsDefault] = useState(existingCount === 0);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (open) {
|
|
17
|
+
setName("");
|
|
18
|
+
setIsDefault(existingCount === 0);
|
|
19
|
+
setError(null);
|
|
20
|
+
}
|
|
21
|
+
}, [open, existingCount]);
|
|
22
|
+
async function handleSubmit() {
|
|
23
|
+
const trimmed = name.trim();
|
|
24
|
+
if (!trimmed) {
|
|
25
|
+
setError(t.validation.nameRequired);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
setError(null);
|
|
29
|
+
try {
|
|
30
|
+
const created = await createPipeline.mutateAsync({
|
|
31
|
+
name: trimmed,
|
|
32
|
+
entityType: "quote",
|
|
33
|
+
isDefault,
|
|
34
|
+
});
|
|
35
|
+
onCreated(created.id);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
setError(err instanceof Error ? err.message : t.validation.createFailed);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t.title }) }), _jsxs("div", { className: "flex flex-col gap-3 py-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "pipeline-name", children: t.nameLabel }), _jsx(Input, { id: "pipeline-name", value: name, onChange: (event) => setName(event.target.value), placeholder: t.namePlaceholder, autoFocus: true })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "pipeline-default", checked: isDefault, onCheckedChange: (checked) => setIsDefault(checked === true) }), _jsx(Label, { htmlFor: "pipeline-default", className: "font-normal text-sm", children: t.setDefault })] }), error ? _jsx("p", { className: "text-destructive text-sm", children: error }) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { onClick: () => void handleSubmit(), disabled: createPipeline.isPending, children: [createPipeline.isPending ? _jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : null, messages.common.create] })] })] }) }));
|
|
42
|
+
}
|
|
43
|
+
export function ManageStagesDialog({ open, onOpenChange, pipelineId, stages, }) {
|
|
44
|
+
const messages = useCrmUiMessagesOrDefault();
|
|
45
|
+
const t = messages.manageStagesDialog;
|
|
46
|
+
const { createStage, updateStage, removeStage } = usePipelineMutation();
|
|
47
|
+
const [newName, setNewName] = useState("");
|
|
48
|
+
const [newProbability, setNewProbability] = useState("");
|
|
49
|
+
const [error, setError] = useState(null);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (open) {
|
|
52
|
+
setNewName("");
|
|
53
|
+
setNewProbability("");
|
|
54
|
+
setError(null);
|
|
55
|
+
}
|
|
56
|
+
}, [open]);
|
|
57
|
+
async function handleAdd() {
|
|
58
|
+
const trimmed = newName.trim();
|
|
59
|
+
if (!trimmed) {
|
|
60
|
+
setError(t.validation.nameRequired);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const probability = newProbability.trim()
|
|
64
|
+
? Math.max(0, Math.min(100, Number.parseInt(newProbability, 10) || 0))
|
|
65
|
+
: null;
|
|
66
|
+
setError(null);
|
|
67
|
+
try {
|
|
68
|
+
await createStage.mutateAsync({
|
|
69
|
+
pipelineId,
|
|
70
|
+
name: trimmed,
|
|
71
|
+
sortOrder: stages.length,
|
|
72
|
+
probability,
|
|
73
|
+
});
|
|
74
|
+
setNewName("");
|
|
75
|
+
setNewProbability("");
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
setError(err instanceof Error ? err.message : t.validation.addFailed);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function handleRename(stageId, name) {
|
|
82
|
+
const trimmed = name.trim();
|
|
83
|
+
if (!trimmed)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
await updateStage.mutateAsync({ id: stageId, input: { name: trimmed } });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// invalidation restores server state
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function handleRemove(stageId) {
|
|
93
|
+
try {
|
|
94
|
+
await removeStage.mutateAsync(stageId);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
setError(err instanceof Error ? err.message : t.validation.removeFailed);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t.title }) }), _jsxs("div", { className: "flex flex-col gap-3 py-2", children: [stages.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: t.empty })) : (_jsx("ul", { className: "divide-y rounded border", children: stages.map((stage) => (_jsxs("li", { className: "flex items-center gap-2 px-2 py-1.5", children: [_jsx(Input, { defaultValue: stage.name, className: "h-8 flex-1 text-sm", onBlur: (event) => {
|
|
101
|
+
const value = event.target.value.trim();
|
|
102
|
+
if (value && value !== stage.name)
|
|
103
|
+
void handleRename(stage.id, value);
|
|
104
|
+
} }), _jsx("span", { className: "w-10 text-right text-muted-foreground text-xs", children: stage.probability != null ? `${stage.probability}%` : messages.common.none }), _jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", onClick: () => void handleRemove(stage.id), disabled: removeStage.isPending, children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }, stage.id))) })), _jsxs("div", { className: "flex flex-col gap-2 rounded border p-2", children: [_jsx("p", { className: "font-medium text-muted-foreground text-xs", children: t.addStageTitle }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: newName, onChange: (event) => setNewName(event.target.value), placeholder: t.stageNamePlaceholder, className: "h-8 flex-1 text-sm" }), _jsx(Input, { value: newProbability, onChange: (event) => setNewProbability(event.target.value), placeholder: t.probabilityPlaceholder, type: "number", min: 0, max: 100, className: "h-8 w-16 text-sm" }), _jsx(Button, { size: "sm", onClick: () => void handleAdd(), disabled: createStage.isPending, children: createStage.isPending ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(Plus, { className: "h-3.5 w-3.5" })) })] })] }), error ? _jsx("p", { className: "text-destructive text-sm", children: error }) : null] }), _jsx(DialogFooter, { children: _jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: messages.common.done }) })] }) }));
|
|
105
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { OrganizationRecord, PersonRecord } from "@voyant-travel/relationships-react";
|
|
2
|
+
import type { CreateQuoteProductInput, QuoteMediaRecord, QuoteParticipantRecord, QuoteProductRecord } from "../index.js";
|
|
3
|
+
export interface QuoteLineItemsCardProps {
|
|
4
|
+
products: QuoteProductRecord[];
|
|
5
|
+
isPending: boolean;
|
|
6
|
+
/** Quote currency, used as the default for new line items. */
|
|
7
|
+
currency: string;
|
|
8
|
+
busy?: boolean;
|
|
9
|
+
onAdd: (input: CreateQuoteProductInput) => Promise<void>;
|
|
10
|
+
onUpdate: (id: string, input: {
|
|
11
|
+
nameSnapshot?: string;
|
|
12
|
+
quantity?: number;
|
|
13
|
+
unitPriceAmountCents?: number | null;
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
onRemove: (id: string) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function QuoteLineItemsCard({ products, isPending, currency, busy, onAdd, onUpdate, onRemove, }: QuoteLineItemsCardProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export interface QuoteTravelersCardProps {
|
|
19
|
+
travelers: QuoteParticipantRecord[];
|
|
20
|
+
/** Explicit headcount — may exceed the number of named travelers. */
|
|
21
|
+
paxCount: number | null;
|
|
22
|
+
isPending: boolean;
|
|
23
|
+
busy?: boolean;
|
|
24
|
+
onPaxCountChange: (paxCount: number | null) => Promise<void>;
|
|
25
|
+
onAdd: (personId: string) => Promise<void>;
|
|
26
|
+
onRemove: (id: string) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare function QuoteTravelersCard({ travelers, paxCount, isPending, busy, onPaxCountChange, onAdd, onRemove, }: QuoteTravelersCardProps): import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
export interface QuoteClientCardProps {
|
|
30
|
+
person: PersonRecord | null | undefined;
|
|
31
|
+
organization: OrganizationRecord | null | undefined;
|
|
32
|
+
busy?: boolean;
|
|
33
|
+
onSetPerson: (personId: string | null) => Promise<void>;
|
|
34
|
+
onSetOrganization: (organizationId: string | null) => Promise<void>;
|
|
35
|
+
onOpenPerson: () => void;
|
|
36
|
+
onOpenOrganization: () => void;
|
|
37
|
+
}
|
|
38
|
+
export declare function QuoteClientCard({ person, organization, busy, onSetPerson, onSetOrganization, onOpenPerson, onOpenOrganization, }: QuoteClientCardProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export interface QuoteOwnershipCardProps {
|
|
40
|
+
ownerId: string | null;
|
|
41
|
+
createdBy: string | null;
|
|
42
|
+
updatedBy: string | null;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
busy?: boolean;
|
|
46
|
+
onSetOwner: (ownerId: string | null) => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export declare function QuoteOwnershipCard({ ownerId, createdBy, updatedBy, createdAt, updatedAt, onSetOwner, }: QuoteOwnershipCardProps): import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
export interface QuoteMediaCardProps {
|
|
50
|
+
media: QuoteMediaRecord[];
|
|
51
|
+
isPending: boolean;
|
|
52
|
+
busy?: boolean;
|
|
53
|
+
onUploadFiles: (files: FileList) => Promise<void>;
|
|
54
|
+
onRemove: (id: string) => Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export declare function QuoteMediaCard({ media, isPending, busy, onUploadFiles, onRemove, }: QuoteMediaCardProps): import("react/jsx-runtime").JSX.Element;
|
|
57
|
+
//# sourceMappingURL=quote-content-sections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-content-sections.d.ts","sourceRoot":"","sources":["../../src/admin/quote-content-sections.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// agent-quality: file-size exception -- owner: quotes-react; the quote-workspace cards (line items, travelers, client, ownership) stay co-located until the staged-edit refactor restructures them.
|
|
2
|
+
"use client";
|
|
3
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { useCurrentUser, useOrganizationMembers } from "@voyant-travel/auth-react";
|
|
5
|
+
import { formatMessage } from "@voyant-travel/i18n";
|
|
6
|
+
import { useOrganizations, usePeople, usePerson } from "@voyant-travel/relationships-react";
|
|
7
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input, } from "@voyant-travel/ui/components";
|
|
8
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyant-travel/ui/components/table";
|
|
9
|
+
import { Building2, Check, ImagePlus, Loader2, Pencil, Plus, Trash2, Upload, User, Users, X, } from "lucide-react";
|
|
10
|
+
import { useEffect, useRef, useState } from "react";
|
|
11
|
+
import { formatCrmDate, formatCrmMoney, formatCrmRelative } from "../components/crm-format.js";
|
|
12
|
+
import { InlineSelectField } from "../components/inline-select-field.js";
|
|
13
|
+
import { useCrmUiI18nOrDefault } from "../i18n/index.js";
|
|
14
|
+
export function QuoteLineItemsCard({ products, isPending, currency, busy, onAdd, onUpdate, onRemove, }) {
|
|
15
|
+
const i18n = useCrmUiI18nOrDefault();
|
|
16
|
+
const t = i18n.messages.quoteLineItemsCard;
|
|
17
|
+
const [name, setName] = useState("");
|
|
18
|
+
const [qty, setQty] = useState("1");
|
|
19
|
+
const [price, setPrice] = useState("");
|
|
20
|
+
const submit = async () => {
|
|
21
|
+
const trimmed = name.trim();
|
|
22
|
+
if (!trimmed)
|
|
23
|
+
return;
|
|
24
|
+
const quantity = Math.max(1, Number.parseInt(qty, 10) || 1);
|
|
25
|
+
const parsedPrice = price.trim() === "" ? null : Math.round(Number.parseFloat(price) * 100);
|
|
26
|
+
await onAdd({
|
|
27
|
+
nameSnapshot: trimmed,
|
|
28
|
+
quantity,
|
|
29
|
+
unitPriceAmountCents: Number.isFinite(parsedPrice) ? parsedPrice : null,
|
|
30
|
+
currency,
|
|
31
|
+
});
|
|
32
|
+
setName("");
|
|
33
|
+
setQty("1");
|
|
34
|
+
setPrice("");
|
|
35
|
+
};
|
|
36
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsx(CardTitle, { children: t.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: t.description })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [isPending ? (_jsx("div", { className: "flex justify-center py-6", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) })) : products.length === 0 ? (_jsx("p", { className: "py-4 text-center text-muted-foreground text-sm", children: t.empty })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t.columns.item }), _jsx(TableHead, { className: "w-16 text-right", children: t.columns.qty }), _jsx(TableHead, { className: "text-right", children: t.columns.unitPrice }), _jsx(TableHead, { className: "text-right", children: t.columns.total }), _jsx(TableHead, { className: "w-10" })] }) }), _jsx(TableBody, { children: products.map((product) => (_jsx(LineItemRow, { product: product, currency: currency, busy: busy, onUpdate: onUpdate, onRemove: onRemove }, product.id))) })] })), _jsxs("div", { className: "flex items-end gap-2 rounded border p-2", children: [_jsx(Input, { value: name, onChange: (event) => setName(event.target.value), placeholder: t.namePlaceholder, className: "h-8 flex-1 text-sm" }), _jsx(Input, { value: qty, onChange: (event) => setQty(event.target.value), type: "number", min: 1, placeholder: t.qtyPlaceholder, className: "h-8 w-16 text-sm" }), _jsx(Input, { value: price, onChange: (event) => setPrice(event.target.value), type: "number", min: 0, step: "0.01", placeholder: t.pricePlaceholder, className: "h-8 w-28 text-sm" }), _jsxs(Button, { size: "sm", onClick: () => void submit(), disabled: busy || name.trim() === "", children: [_jsx(Plus, { className: "mr-1.5 h-4 w-4" }), t.addItem] })] })] })] }));
|
|
37
|
+
}
|
|
38
|
+
function LineItemRow({ product, currency, busy, onUpdate, onRemove, }) {
|
|
39
|
+
const i18n = useCrmUiI18nOrDefault();
|
|
40
|
+
const t = i18n.messages.quoteLineItemsCard;
|
|
41
|
+
const rowCurrency = product.currency ?? currency;
|
|
42
|
+
const [editing, setEditing] = useState(false);
|
|
43
|
+
const [name, setName] = useState(product.nameSnapshot);
|
|
44
|
+
const [qty, setQty] = useState(String(product.quantity));
|
|
45
|
+
const [price, setPrice] = useState(product.unitPriceAmountCents != null ? String(product.unitPriceAmountCents / 100) : "");
|
|
46
|
+
const startEditing = () => {
|
|
47
|
+
setName(product.nameSnapshot);
|
|
48
|
+
setQty(String(product.quantity));
|
|
49
|
+
setPrice(product.unitPriceAmountCents != null ? String(product.unitPriceAmountCents / 100) : "");
|
|
50
|
+
setEditing(true);
|
|
51
|
+
};
|
|
52
|
+
const save = async () => {
|
|
53
|
+
const trimmed = name.trim();
|
|
54
|
+
if (!trimmed)
|
|
55
|
+
return;
|
|
56
|
+
const quantity = Math.max(1, Number.parseInt(qty, 10) || 1);
|
|
57
|
+
const parsedPrice = price.trim() === "" ? null : Math.round(Number.parseFloat(price) * 100);
|
|
58
|
+
await onUpdate(product.id, {
|
|
59
|
+
nameSnapshot: trimmed,
|
|
60
|
+
quantity,
|
|
61
|
+
unitPriceAmountCents: Number.isFinite(parsedPrice) ? parsedPrice : null,
|
|
62
|
+
});
|
|
63
|
+
setEditing(false);
|
|
64
|
+
};
|
|
65
|
+
if (editing) {
|
|
66
|
+
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Input, { value: name, onChange: (event) => setName(event.target.value), className: "h-8 text-sm" }) }), _jsx(TableCell, { children: _jsx(Input, { value: qty, onChange: (event) => setQty(event.target.value), type: "number", min: 1, className: "h-8 w-16 text-right text-sm" }) }), _jsx(TableCell, { children: _jsx(Input, { value: price, onChange: (event) => setPrice(event.target.value), type: "number", min: 0, step: "0.01", className: "h-8 w-24 text-right text-sm" }) }), _jsx(TableCell, { colSpan: 2, children: _jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", disabled: busy, onClick: () => void save(), children: _jsx(Check, { className: "h-3.5 w-3.5" }) }), _jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", disabled: busy, onClick: () => setEditing(false), children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }) })] }));
|
|
67
|
+
}
|
|
68
|
+
const lineTotal = (product.unitPriceAmountCents ?? 0) * product.quantity - (product.discountAmountCents ?? 0);
|
|
69
|
+
return (_jsxs(TableRow, { className: "group", children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: product.nameSnapshot }), product.description ? (_jsx("div", { className: "text-muted-foreground text-xs", children: product.description })) : null] }), _jsx(TableCell, { className: "text-right", children: product.quantity }), _jsx(TableCell, { className: "text-right", children: formatCrmMoney(i18n, product.unitPriceAmountCents, rowCurrency) }), _jsx(TableCell, { className: "text-right font-medium", children: formatCrmMoney(i18n, lineTotal, rowCurrency) }), _jsx(TableCell, { children: _jsxs("div", { className: "flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100", children: [_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", disabled: busy, onClick: startEditing, "aria-label": t.editItem, children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", disabled: busy, onClick: () => void onRemove(product.id), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }));
|
|
70
|
+
}
|
|
71
|
+
export function QuoteTravelersCard({ travelers, paxCount, isPending, busy, onPaxCountChange, onAdd, onRemove, }) {
|
|
72
|
+
const i18n = useCrmUiI18nOrDefault();
|
|
73
|
+
const t = i18n.messages.quoteTravelersCard;
|
|
74
|
+
const [search, setSearch] = useState("");
|
|
75
|
+
const [paxDraft, setPaxDraft] = useState(paxCount != null ? String(paxCount) : "");
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
setPaxDraft(paxCount != null ? String(paxCount) : "");
|
|
78
|
+
}, [paxCount]);
|
|
79
|
+
const commitPax = () => {
|
|
80
|
+
const trimmed = paxDraft.trim();
|
|
81
|
+
const next = trimmed === "" ? null : Math.max(0, Number.parseInt(trimmed, 10) || 0);
|
|
82
|
+
if (next !== paxCount)
|
|
83
|
+
void onPaxCountChange(next);
|
|
84
|
+
};
|
|
85
|
+
const peopleQuery = usePeople({ search, limit: 6, enabled: search.trim().length >= 2 });
|
|
86
|
+
const existingPersonIds = new Set(travelers.map((traveler) => traveler.personId));
|
|
87
|
+
const results = (peopleQuery.data?.data ?? []).filter((person) => !existingPersonIds.has(person.id));
|
|
88
|
+
const add = async (personId) => {
|
|
89
|
+
await onAdd(personId);
|
|
90
|
+
setSearch("");
|
|
91
|
+
};
|
|
92
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between gap-2 pb-3", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(Users, { className: "h-4 w-4 text-muted-foreground" }), t.title] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Input, { type: "number", min: 0, value: paxDraft, disabled: busy, onChange: (event) => setPaxDraft(event.target.value), onBlur: commitPax, onKeyDown: (event) => {
|
|
93
|
+
if (event.key === "Enter") {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
commitPax();
|
|
96
|
+
}
|
|
97
|
+
}, placeholder: String(travelers.length), "aria-label": t.paxLabel, className: "h-7 w-14 text-right text-sm" }), _jsx("span", { className: "text-muted-foreground text-xs", children: t.paxLabel })] })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [isPending ? (_jsx("div", { className: "flex justify-center py-4", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) })) : travelers.length === 0 ? (_jsx("p", { className: "py-2 text-center text-muted-foreground text-sm", children: t.empty })) : (_jsx("ul", { className: "divide-y", children: travelers.map((traveler) => (_jsx(TravelerRow, { traveler: traveler, busy: busy, onRemove: () => void onRemove(traveler.id) }, traveler.id))) })), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Input, { value: search, onChange: (event) => setSearch(event.target.value), placeholder: t.addPlaceholder, className: "h-8 text-sm" }), search.trim().length >= 2 ? (_jsx("div", { className: "rounded border", children: peopleQuery.isPending ? (_jsx("div", { className: "flex justify-center py-2", children: _jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" }) })) : results.length === 0 ? (_jsx("p", { className: "py-2 text-center text-muted-foreground text-xs", children: t.noResults })) : (_jsx("ul", { className: "max-h-40 overflow-auto", children: results.map((person) => (_jsx("li", { children: _jsxs("button", { type: "button", disabled: busy, onClick: () => void add(person.id), className: "flex w-full items-center justify-between px-2 py-1.5 text-left text-sm hover:bg-muted/40", children: [_jsx("span", { className: "truncate", children: [person.firstName, person.lastName].filter(Boolean).join(" ") ||
|
|
98
|
+
person.id }), _jsx(Plus, { className: "h-3.5 w-3.5 text-muted-foreground" })] }) }, person.id))) })) })) : null] })] })] }));
|
|
99
|
+
}
|
|
100
|
+
function TravelerRow({ traveler, busy, onRemove, }) {
|
|
101
|
+
const personQuery = usePerson(traveler.personId, { enabled: Boolean(traveler.personId) });
|
|
102
|
+
const person = personQuery.data;
|
|
103
|
+
const name = person
|
|
104
|
+
? [person.firstName, person.lastName].filter(Boolean).join(" ").trim() || traveler.personId
|
|
105
|
+
: traveler.personId;
|
|
106
|
+
return (_jsxs("li", { className: "flex items-center justify-between gap-2 py-2 text-sm", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "truncate font-medium", children: name }), traveler.isPrimary ? (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: "\u2605" })) : null] }), _jsx(Button, { size: "sm", variant: "ghost", className: "h-7 w-7 shrink-0 p-0", disabled: busy, onClick: onRemove, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }));
|
|
107
|
+
}
|
|
108
|
+
export function QuoteClientCard({ person, organization, busy, onSetPerson, onSetOrganization, onOpenPerson, onOpenOrganization, }) {
|
|
109
|
+
const i18n = useCrmUiI18nOrDefault();
|
|
110
|
+
const t = i18n.messages.quoteClientCard;
|
|
111
|
+
const personName = person
|
|
112
|
+
? [person.firstName, person.lastName].filter(Boolean).join(" ").trim() || person.id
|
|
113
|
+
: null;
|
|
114
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "font-semibold text-sm", children: t.title }) }), _jsxs(CardContent, { className: "flex flex-col gap-4 text-sm", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("div", { className: "font-medium text-muted-foreground text-xs", children: t.contactLabel }), person ? (_jsx(SelectedEntity, { icon: User, name: personName ?? person.id, subtitle: person.jobTitle, busy: busy, onOpen: onOpenPerson, onClear: () => void onSetPerson(null) })) : (_jsx(PersonPicker, { placeholder: t.searchContact, noResults: t.noResults, busy: busy, onSelect: (id) => void onSetPerson(id) }))] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("div", { className: "font-medium text-muted-foreground text-xs", children: t.companyLabel }), organization ? (_jsx(SelectedEntity, { icon: Building2, name: organization.name, subtitle: organization.industry, busy: busy, onOpen: onOpenOrganization, onClear: () => void onSetOrganization(null) })) : (_jsx(OrgPicker, { placeholder: t.searchCompany, noResults: t.noResults, busy: busy, onSelect: (id) => void onSetOrganization(id) }))] })] })] }));
|
|
115
|
+
}
|
|
116
|
+
function SelectedEntity({ icon: Icon, name, subtitle, busy, onOpen, onClear, }) {
|
|
117
|
+
return (_jsxs("div", { className: "flex items-center gap-2 rounded border p-2", children: [_jsx(Icon, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsxs("button", { type: "button", onClick: onOpen, className: "min-w-0 flex-1 text-left hover:underline", children: [_jsx("span", { className: "truncate font-medium", children: name }), subtitle ? (_jsx("span", { className: "block truncate text-muted-foreground text-xs", children: subtitle })) : null] }), _jsx(Button, { size: "sm", variant: "ghost", className: "h-7 w-7 shrink-0 p-0", disabled: busy, onClick: onClear, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }));
|
|
118
|
+
}
|
|
119
|
+
function PersonPicker({ placeholder, noResults, busy, onSelect, }) {
|
|
120
|
+
const [search, setSearch] = useState("");
|
|
121
|
+
const query = usePeople({ search, limit: 6, enabled: search.trim().length >= 2 });
|
|
122
|
+
const results = query.data?.data ?? [];
|
|
123
|
+
return (_jsx(EntitySearch, { search: search, onSearch: setSearch, isPending: query.isPending, placeholder: placeholder, noResults: noResults, busy: busy, results: results.map((person) => ({
|
|
124
|
+
id: person.id,
|
|
125
|
+
label: [person.firstName, person.lastName].filter(Boolean).join(" ") || person.id,
|
|
126
|
+
})), onSelect: (id) => {
|
|
127
|
+
onSelect(id);
|
|
128
|
+
setSearch("");
|
|
129
|
+
} }));
|
|
130
|
+
}
|
|
131
|
+
function OrgPicker({ placeholder, noResults, busy, onSelect, }) {
|
|
132
|
+
const [search, setSearch] = useState("");
|
|
133
|
+
const query = useOrganizations({ search, limit: 6, enabled: search.trim().length >= 2 });
|
|
134
|
+
const results = query.data?.data ?? [];
|
|
135
|
+
return (_jsx(EntitySearch, { search: search, onSearch: setSearch, isPending: query.isPending, placeholder: placeholder, noResults: noResults, busy: busy, results: results.map((organization) => ({ id: organization.id, label: organization.name })), onSelect: (id) => {
|
|
136
|
+
onSelect(id);
|
|
137
|
+
setSearch("");
|
|
138
|
+
} }));
|
|
139
|
+
}
|
|
140
|
+
function EntitySearch({ search, onSearch, isPending, placeholder, noResults, busy, results, onSelect, }) {
|
|
141
|
+
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Input, { value: search, onChange: (event) => onSearch(event.target.value), placeholder: placeholder, className: "h-8 text-sm" }), search.trim().length >= 2 ? (_jsx("div", { className: "rounded border", children: isPending ? (_jsx("div", { className: "flex justify-center py-2", children: _jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" }) })) : results.length === 0 ? (_jsx("p", { className: "py-2 text-center text-muted-foreground text-xs", children: noResults })) : (_jsx("ul", { className: "max-h-40 overflow-auto", children: results.map((result) => (_jsx("li", { children: _jsxs("button", { type: "button", disabled: busy, onClick: () => onSelect(result.id), className: "flex w-full items-center justify-between px-2 py-1.5 text-left text-sm hover:bg-muted/40", children: [_jsx("span", { className: "truncate", children: result.label }), _jsx(Plus, { className: "h-3.5 w-3.5 text-muted-foreground" })] }) }, result.id))) })) })) : null] }));
|
|
142
|
+
}
|
|
143
|
+
export function QuoteOwnershipCard({ ownerId, createdBy, updatedBy, createdAt, updatedAt, onSetOwner, }) {
|
|
144
|
+
const i18n = useCrmUiI18nOrDefault();
|
|
145
|
+
const t = i18n.messages.quoteOwnershipCard;
|
|
146
|
+
const membersQuery = useOrganizationMembers();
|
|
147
|
+
const currentUserQuery = useCurrentUser();
|
|
148
|
+
const currentUser = currentUserQuery.data;
|
|
149
|
+
// Candidate owners: the org's team members when that endpoint is available,
|
|
150
|
+
// plus the current user (always — so an owner is assignable even in cloud
|
|
151
|
+
// deployments that don't expose a members list). Keyed by userId.
|
|
152
|
+
const candidates = new Map();
|
|
153
|
+
for (const member of membersQuery.data?.members ?? []) {
|
|
154
|
+
candidates.set(member.userId, member.user.name ?? member.user.email ?? member.userId);
|
|
155
|
+
}
|
|
156
|
+
if (currentUser) {
|
|
157
|
+
const currentName = [currentUser.firstName, currentUser.lastName].filter(Boolean).join(" ").trim() ||
|
|
158
|
+
currentUser.email ||
|
|
159
|
+
currentUser.id;
|
|
160
|
+
if (!candidates.has(currentUser.id))
|
|
161
|
+
candidates.set(currentUser.id, currentName);
|
|
162
|
+
}
|
|
163
|
+
const nameOf = (userId) => {
|
|
164
|
+
if (!userId)
|
|
165
|
+
return t.unassigned;
|
|
166
|
+
return candidates.get(userId) ?? userId;
|
|
167
|
+
};
|
|
168
|
+
const options = [...candidates.entries()].map(([value, label]) => ({ value, label }));
|
|
169
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "font-semibold text-sm", children: t.title }) }), _jsxs(CardContent, { className: "flex flex-col gap-2 text-sm", children: [_jsx(InlineSelectField, { icon: User, label: t.ownerLabel, value: ownerId, options: options, onSave: (next) => onSetOwner(next) }), _jsxs("div", { className: "flex flex-col gap-0.5 border-t pt-2 text-muted-foreground text-xs", children: [_jsxs("span", { children: [formatMessage(t.createdBy, { name: nameOf(createdBy) }), " \u00B7", " ", formatCrmDate(i18n, createdAt)] }), _jsxs("span", { children: [formatMessage(t.updatedBy, { name: nameOf(updatedBy) }), " \u00B7", " ", formatCrmRelative(i18n, updatedAt)] })] })] })] }));
|
|
170
|
+
}
|
|
171
|
+
export function QuoteMediaCard({ media, isPending, busy, onUploadFiles, onRemove, }) {
|
|
172
|
+
const i18n = useCrmUiI18nOrDefault();
|
|
173
|
+
const t = i18n.messages.quoteMediaCard;
|
|
174
|
+
const inputRef = useRef(null);
|
|
175
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between gap-2 pb-3", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: t.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: t.description })] }), _jsxs(Button, { size: "sm", variant: "outline", disabled: busy, onClick: () => inputRef.current?.click(), children: [busy ? (_jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" })) : (_jsx(Upload, { className: "mr-1.5 h-4 w-4" })), t.add] }), _jsx("input", { ref: inputRef, type: "file", accept: "image/*", multiple: true, className: "hidden", onChange: (event) => {
|
|
176
|
+
const files = event.target.files;
|
|
177
|
+
if (files && files.length > 0)
|
|
178
|
+
void onUploadFiles(files);
|
|
179
|
+
event.target.value = "";
|
|
180
|
+
} })] }), _jsx(CardContent, { children: isPending ? (_jsx("div", { className: "flex justify-center py-6", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) })) : media.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center gap-2 py-8 text-center text-muted-foreground", children: [_jsx(ImagePlus, { className: "h-6 w-6" }), _jsx("p", { className: "text-sm", children: t.empty })] })) : (_jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3", children: media.map((item) => (_jsxs("div", { className: "group relative overflow-hidden rounded border", children: [item.mediaType === "image" ? (_jsx("img", { src: item.url, alt: item.altText ?? item.name, className: "aspect-video w-full object-cover" })) : (_jsx("div", { className: "flex aspect-video w-full items-center justify-center bg-muted text-muted-foreground text-xs", children: item.name })), _jsx("button", { type: "button", disabled: busy, onClick: () => void onRemove(item.id), className: "absolute top-1 right-1 rounded-full bg-background/80 p-1 opacity-0 transition group-hover:opacity-100", children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }, item.id))) })) })] }));
|
|
181
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ActivityRecord } from "@voyant-travel/relationships-react";
|
|
2
|
+
interface QuoteDetailsFields {
|
|
3
|
+
title: string;
|
|
4
|
+
stageId: string;
|
|
5
|
+
status: string;
|
|
6
|
+
valueAmountCents: number | null;
|
|
7
|
+
valueCurrency: string | null;
|
|
8
|
+
expectedCloseDate: string | null;
|
|
9
|
+
source: string | null;
|
|
10
|
+
sourceRef: string | null;
|
|
11
|
+
lostReason: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface QuoteDetailsCardProps {
|
|
14
|
+
quote: QuoteDetailsFields;
|
|
15
|
+
stages: ReadonlyArray<{
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}>;
|
|
19
|
+
onUpdateField: (patch: Record<string, unknown>) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** Editable deal fields — every value flows back through the quote update mutation. */
|
|
22
|
+
export declare function QuoteDetailsCard({ quote, stages, onUpdateField }: QuoteDetailsCardProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export interface QuoteTagsCardProps {
|
|
24
|
+
tags: string[] | null | undefined;
|
|
25
|
+
onChange: (tags: string[]) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export declare function QuoteTagsCard({ tags, onChange }: QuoteTagsCardProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export interface QuoteActivitiesCardProps {
|
|
29
|
+
isPending: boolean;
|
|
30
|
+
activities: ActivityRecord[];
|
|
31
|
+
}
|
|
32
|
+
export declare function QuoteActivitiesCard({ isPending, activities }: QuoteActivitiesCardProps): import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=quote-detail-sections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-detail-sections.d.ts","sourceRoot":"","sources":["../../src/admin/quote-detail-sections.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AAkBxE,UAAU,kBAAkB;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,kBAAkB,CAAA;IACzB,MAAM,EAAE,aAAa,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACnD,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACjE;AAED,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,qBAAqB,2CA4EvF;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAA;IACjC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5C;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,kBAAkB,2CA4DnE;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,cAAc,EAAE,CAAA;CAC7B;AAED,wBAAgB,mBAAmB,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,wBAAwB,2CAyCtF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge, Card, CardContent, CardHeader, CardTitle, Input, } from "@voyant-travel/ui/components";
|
|
4
|
+
import { Calendar, CircleDot, DollarSign, Tag, Target, X } from "lucide-react";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { formatCrmMoney, formatCrmRelative } from "../components/crm-format.js";
|
|
7
|
+
import { InlineCurrencyField } from "../components/inline-currency-field.js";
|
|
8
|
+
import { InlineField } from "../components/inline-field.js";
|
|
9
|
+
import { InlineSelectField } from "../components/inline-select-field.js";
|
|
10
|
+
import { useCrmUiI18nOrDefault, useCrmUiMessagesOrDefault } from "../i18n/index.js";
|
|
11
|
+
import { crmQuoteStatuses } from "../i18n/messages.js";
|
|
12
|
+
/** Editable deal fields — every value flows back through the quote update mutation. */
|
|
13
|
+
export function QuoteDetailsCard({ quote, stages, onUpdateField }) {
|
|
14
|
+
const i18n = useCrmUiI18nOrDefault();
|
|
15
|
+
const { messages } = i18n;
|
|
16
|
+
const t = messages.quoteDetailPage;
|
|
17
|
+
const statusOptions = crmQuoteStatuses.map((status) => ({
|
|
18
|
+
value: status,
|
|
19
|
+
label: messages.common.quoteStatusLabels[status] ?? status,
|
|
20
|
+
}));
|
|
21
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "font-semibold text-sm", children: t.detailsTitle }) }), _jsxs(CardContent, { className: "divide-y text-sm", children: [_jsx(InlineField, { icon: Target, label: t.fields.title, value: quote.title, onSave: (next) => onUpdateField({ title: next ?? "" }) }), _jsx(InlineSelectField, { icon: CircleDot, label: t.fields.stage, value: quote.stageId, options: stages.map((stage) => ({ value: stage.id, label: stage.name })), allowClear: false, onSave: (next) => onUpdateField({ stageId: next ?? quote.stageId }) }), _jsx(InlineSelectField, { icon: CircleDot, label: t.fields.status, value: quote.status, options: statusOptions, allowClear: false, onSave: (next) => onUpdateField({ status: next ?? quote.status }) }), _jsxs("div", { className: "flex items-start gap-3 py-1.5", children: [_jsx(DollarSign, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium text-muted-foreground text-xs", children: t.fields.value }), _jsx("div", { className: "text-sm", children: formatCrmMoney(i18n, quote.valueAmountCents, quote.valueCurrency) }), _jsx("div", { className: "text-[10px] text-muted-foreground", children: t.fields.valueHint })] })] }), _jsx(InlineCurrencyField, { label: t.fields.currency, value: quote.valueCurrency, onSave: (next) => onUpdateField({ valueCurrency: next }) }), _jsx(InlineField, { icon: Calendar, label: t.fields.expectedClose, placeholder: "YYYY-MM-DD", value: quote.expectedCloseDate, onSave: (next) => onUpdateField({ expectedCloseDate: next }) }), _jsx(InlineField, { icon: Tag, label: t.fields.source, value: quote.source, onSave: (next) => onUpdateField({ source: next }) }), quote.status === "lost" ? (_jsx(InlineField, { label: t.fields.lostReason, kind: "textarea", value: quote.lostReason, onSave: (next) => onUpdateField({ lostReason: next }) })) : null] })] }));
|
|
22
|
+
}
|
|
23
|
+
export function QuoteTagsCard({ tags, onChange }) {
|
|
24
|
+
const messages = useCrmUiMessagesOrDefault();
|
|
25
|
+
const t = messages.quoteDetailPage;
|
|
26
|
+
const current = tags ?? [];
|
|
27
|
+
const [draft, setDraft] = useState("");
|
|
28
|
+
const addTag = async () => {
|
|
29
|
+
const value = draft.trim();
|
|
30
|
+
if (!value || current.includes(value)) {
|
|
31
|
+
setDraft("");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
setDraft("");
|
|
35
|
+
await onChange([...current, value]);
|
|
36
|
+
};
|
|
37
|
+
const removeTag = async (tag) => {
|
|
38
|
+
await onChange(current.filter((entry) => entry !== tag));
|
|
39
|
+
};
|
|
40
|
+
const onKeyDown = (event) => {
|
|
41
|
+
if (event.key === "Enter") {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
void addTag();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "font-semibold text-sm", children: t.tagsTitle }) }), _jsxs(CardContent, { className: "flex flex-col gap-2", children: [current.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-1.5", children: current.map((tag) => (_jsxs(Badge, { variant: "secondary", className: "gap-1", children: [tag, _jsx("button", { type: "button", onClick: () => void removeTag(tag), className: "rounded-sm hover:text-destructive", children: _jsx(X, { className: "size-3" }) })] }, tag))) })) : null, _jsx(Input, { value: draft, onChange: (event) => setDraft(event.target.value), onKeyDown: onKeyDown, onBlur: () => void addTag(), placeholder: t.addTagPlaceholder, className: "h-8 text-sm" })] })] }));
|
|
47
|
+
}
|
|
48
|
+
export function QuoteActivitiesCard({ isPending, activities }) {
|
|
49
|
+
const i18n = useCrmUiI18nOrDefault();
|
|
50
|
+
const t = i18n.messages.quoteDetailPage;
|
|
51
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "font-semibold text-sm", children: t.activitiesTitle }) }), _jsx(CardContent, { children: isPending ? null : activities.length === 0 ? (_jsx("p", { className: "py-4 text-center text-muted-foreground text-sm", children: t.activitiesEmpty })) : (_jsx("ul", { className: "divide-y", children: activities.map((activity) => (_jsx("li", { className: "py-3", children: _jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate font-medium text-sm", children: activity.subject }), activity.description ? (_jsx("p", { className: "mt-1 whitespace-pre-wrap text-muted-foreground text-sm", children: activity.description })) : null] }), _jsxs("div", { className: "shrink-0 text-right text-muted-foreground text-xs", children: [_jsx("p", { children: i18n.messages.common.activityTypeLabels[activity.type] ?? activity.type }), _jsx("p", { children: formatCrmRelative(i18n, activity.createdAt) })] })] }) }, activity.id))) })) })] }));
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaged admin host for the Quotes board (packaged-admin RFC Phase 3). The
|
|
3
|
+
* landing surface for the quotes domain: pick a pipeline, manage its stages,
|
|
4
|
+
* create quotes, and open a quote's detail (where its versions live). All
|
|
5
|
+
* data flows through the shared `@voyant-travel/react` provider context
|
|
6
|
+
* mounted by the workspace shell; cross-route links resolve through the
|
|
7
|
+
* `quote.detail` semantic destination.
|
|
8
|
+
*/
|
|
9
|
+
export declare function QuotesBoardHost(): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=quotes-board-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quotes-board-host.d.ts","sourceRoot":"","sources":["../../src/admin/quotes-board-host.tsx"],"names":[],"mappings":"AAwBA;;;;;;;GAOG;AACH,wBAAgB,eAAe,4CAoK9B"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useAdminNavigate } from "@voyant-travel/admin";
|
|
4
|
+
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyant-travel/ui/components";
|
|
5
|
+
import { Loader2, Plus, Settings2 } from "lucide-react";
|
|
6
|
+
import { useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { QuotesBoard } from "../components/quotes-board.js";
|
|
8
|
+
import { useCrmUiMessagesOrDefault } from "../i18n/index.js";
|
|
9
|
+
import { CreateQuoteDialog, usePipelines, useQuotes, useStages, } from "../index.js";
|
|
10
|
+
import { CreatePipelineDialog, ManageStagesDialog } from "./pipeline-dialogs.js";
|
|
11
|
+
/**
|
|
12
|
+
* Packaged admin host for the Quotes board (packaged-admin RFC Phase 3). The
|
|
13
|
+
* landing surface for the quotes domain: pick a pipeline, manage its stages,
|
|
14
|
+
* create quotes, and open a quote's detail (where its versions live). All
|
|
15
|
+
* data flows through the shared `@voyant-travel/react` provider context
|
|
16
|
+
* mounted by the workspace shell; cross-route links resolve through the
|
|
17
|
+
* `quote.detail` semantic destination.
|
|
18
|
+
*/
|
|
19
|
+
export function QuotesBoardHost() {
|
|
20
|
+
const messages = useCrmUiMessagesOrDefault();
|
|
21
|
+
const t = messages.quotesBoardPage;
|
|
22
|
+
const navigate = useAdminNavigate();
|
|
23
|
+
const pipelinesQuery = usePipelines({ entityType: "quote", limit: 50 });
|
|
24
|
+
const pipelines = pipelinesQuery.data?.data ?? [];
|
|
25
|
+
const defaultPipelineId = (pipelines.find((pipeline) => pipeline.isDefault) ?? pipelines[0])?.id ?? null;
|
|
26
|
+
const [selectedPipelineId, setSelectedPipelineId] = useState(defaultPipelineId);
|
|
27
|
+
const [showCreatePipeline, setShowCreatePipeline] = useState(false);
|
|
28
|
+
const [showManageStages, setShowManageStages] = useState(false);
|
|
29
|
+
const [showCreateQuote, setShowCreateQuote] = useState(false);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!selectedPipelineId && defaultPipelineId) {
|
|
32
|
+
setSelectedPipelineId(defaultPipelineId);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (selectedPipelineId && !pipelines.some((pipeline) => pipeline.id === selectedPipelineId)) {
|
|
36
|
+
setSelectedPipelineId(defaultPipelineId);
|
|
37
|
+
}
|
|
38
|
+
}, [defaultPipelineId, pipelines, selectedPipelineId]);
|
|
39
|
+
const stagesQuery = useStages({
|
|
40
|
+
pipelineId: selectedPipelineId ?? undefined,
|
|
41
|
+
limit: 100,
|
|
42
|
+
enabled: Boolean(selectedPipelineId),
|
|
43
|
+
});
|
|
44
|
+
const quotesQuery = useQuotes({
|
|
45
|
+
pipelineId: selectedPipelineId ?? undefined,
|
|
46
|
+
status: "open",
|
|
47
|
+
limit: 100,
|
|
48
|
+
enabled: Boolean(selectedPipelineId),
|
|
49
|
+
});
|
|
50
|
+
const stages = useMemo(() => [...(stagesQuery.data?.data ?? [])].sort((left, right) => left.sortOrder - right.sortOrder), [stagesQuery.data]);
|
|
51
|
+
const quotes = quotesQuery.data?.data ?? [];
|
|
52
|
+
const quotesByStage = useMemo(() => {
|
|
53
|
+
const map = new Map();
|
|
54
|
+
for (const stage of stages)
|
|
55
|
+
map.set(stage.id, []);
|
|
56
|
+
for (const quote of quotes) {
|
|
57
|
+
map.get(quote.stageId)?.push(quote);
|
|
58
|
+
}
|
|
59
|
+
return map;
|
|
60
|
+
}, [stages, quotes]);
|
|
61
|
+
const openQuote = (quoteId) => navigate("quote.detail", { quoteId });
|
|
62
|
+
return (_jsxs("div", { className: "flex h-full min-w-0 flex-col gap-4 p-4 lg:p-6", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "font-bold text-2xl tracking-tight", children: t.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: t.description })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Select, { value: selectedPipelineId ?? undefined, onValueChange: (value) => setSelectedPipelineId(value), disabled: pipelinesQuery.isPending || pipelines.length === 0, children: [_jsx(SelectTrigger, { className: "w-[200px] text-sm", children: _jsx(SelectValue, { placeholder: t.selectPipelinePlaceholder }) }), _jsx(SelectContent, { children: pipelines.map((pipeline) => (_jsxs(SelectItem, { value: pipeline.id, children: [pipeline.name, pipeline.isDefault ? t.defaultSuffix : ""] }, pipeline.id))) })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setShowCreatePipeline(true), disabled: pipelinesQuery.isPending, children: [_jsx(Plus, { className: "mr-1.5 h-4 w-4" }), t.newPipeline] }), selectedPipelineId ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setShowManageStages(true), disabled: stagesQuery.isPending, children: [_jsx(Settings2, { className: "mr-1.5 h-4 w-4" }), t.manageStages] }), _jsxs(Button, { size: "sm", onClick: () => setShowCreateQuote(true), disabled: stagesQuery.isPending || stages.length === 0, children: [_jsx(Plus, { className: "mr-1.5 h-4 w-4" }), t.newQuote] })] })) : null] })] }), pipelinesQuery.isPending ? (_jsx("div", { className: "flex flex-1 items-center justify-center", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : pipelines.length === 0 ? (_jsx(EmptyState, { title: t.emptyNoPipelines.title, description: t.emptyNoPipelines.description })) : !selectedPipelineId ? null : stagesQuery.isPending ? (_jsx("div", { className: "flex flex-1 items-center justify-center", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : stages.length === 0 ? (_jsx(EmptyState, { title: t.emptyNoStages.title, description: t.emptyNoStages.description })) : (_jsx(QuotesBoard, { stages: stages, quotesByStage: quotesByStage, onQuoteOpen: (quote) => openQuote(quote.id) })), _jsx(CreatePipelineDialog, { open: showCreatePipeline, onOpenChange: setShowCreatePipeline, existingCount: pipelines.length, onCreated: (pipelineId) => {
|
|
63
|
+
setSelectedPipelineId(pipelineId);
|
|
64
|
+
setShowCreatePipeline(false);
|
|
65
|
+
} }), selectedPipelineId ? (_jsxs(_Fragment, { children: [_jsx(ManageStagesDialog, { open: showManageStages, onOpenChange: setShowManageStages, pipelineId: selectedPipelineId, stages: stages }), _jsx(CreateQuoteDialog, { open: showCreateQuote, onOpenChange: setShowCreateQuote, pipelineId: selectedPipelineId, stages: stages, onCreated: (quoteId) => {
|
|
66
|
+
setShowCreateQuote(false);
|
|
67
|
+
openQuote(quoteId);
|
|
68
|
+
} })] })) : null] }));
|
|
69
|
+
}
|
|
70
|
+
function EmptyState({ title, description }) {
|
|
71
|
+
return (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-1 py-12 text-center", children: [_jsx("p", { className: "font-medium text-sm", children: title }), _jsx("p", { className: "text-muted-foreground text-sm", children: description })] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface CreateQuoteMediaInput {
|
|
2
|
+
mediaType?: "image" | "video" | "document";
|
|
3
|
+
name: string;
|
|
4
|
+
url: string;
|
|
5
|
+
storageKey?: string | null;
|
|
6
|
+
mimeType?: string | null;
|
|
7
|
+
fileSize?: number | null;
|
|
8
|
+
altText?: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** Attach / remove quote media (the upload itself goes through `/v1/uploads`). */
|
|
11
|
+
export declare function useQuoteMediaMutation(): {
|
|
12
|
+
create: import("@tanstack/react-query").UseMutationResult<{
|
|
13
|
+
id: string;
|
|
14
|
+
quoteId: string;
|
|
15
|
+
mediaType: string;
|
|
16
|
+
name: string;
|
|
17
|
+
url: string;
|
|
18
|
+
storageKey: string | null;
|
|
19
|
+
mimeType: string | null;
|
|
20
|
+
fileSize: number | null;
|
|
21
|
+
altText: string | null;
|
|
22
|
+
sortOrder: number;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
}, Error, {
|
|
26
|
+
quoteId: string;
|
|
27
|
+
input: CreateQuoteMediaInput;
|
|
28
|
+
}, unknown>;
|
|
29
|
+
upload: import("@tanstack/react-query").UseMutationResult<{
|
|
30
|
+
id: string;
|
|
31
|
+
quoteId: string;
|
|
32
|
+
mediaType: string;
|
|
33
|
+
name: string;
|
|
34
|
+
url: string;
|
|
35
|
+
storageKey: string | null;
|
|
36
|
+
mimeType: string | null;
|
|
37
|
+
fileSize: number | null;
|
|
38
|
+
altText: string | null;
|
|
39
|
+
sortOrder: number;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
}, Error, {
|
|
43
|
+
quoteId: string;
|
|
44
|
+
file: File;
|
|
45
|
+
}, unknown>;
|
|
46
|
+
remove: import("@tanstack/react-query").UseMutationResult<void, Error, {
|
|
47
|
+
id: string;
|
|
48
|
+
quoteId: string;
|
|
49
|
+
}, unknown>;
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=use-quote-media-mutation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-quote-media-mutation.d.ts","sourceRoot":"","sources":["../../src/hooks/use-quote-media-mutation.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,UAAU,CAAA;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,kFAAkF;AAClF,wBAAgB,qBAAqB;;;;;;;;;;;;;;;iBASiB,MAAM;eAAS,qBAAqB;;;;;;;;;;;;;;;;iBAgBrC,MAAM;cAAQ,IAAI;;;YA0ClC,MAAM;iBAAW,MAAM;;EAY3D"}
|