@wisemen/wise-crm-web 2.0.0-alpha.4 → 2.0.0-alpha.40

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/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @wisemen/wise-crm-web
2
+
3
+ Vue 3 CRM package: business, individual and deal overviews, detail views, dialogs and widgets.
4
+
5
+ ## Extending the detail views
6
+
7
+ The business and individual detail views can be extended with content from the host app in two places.
8
+
9
+ ### Custom tabs
10
+
11
+ Tabs next to the detail sidebar are routes, registered through `useWiseCrmRoutes`. At least one tab is required for businesses and individuals. The tab component reads the entity uuid from the route params (`businessUuid` / `individualUuid` / `dealUuid`).
12
+
13
+ ```ts
14
+ useWiseCrmRoutes({
15
+ businessTabRoutes: [
16
+ {
17
+ label: computed<string>(() => t('crm.business.tabs.information')),
18
+ route: {
19
+ name: 'crm-business-information',
20
+ path: 'information',
21
+ component: (): Component => import('./CrmBusinessInformationTabView.vue'),
22
+ },
23
+ },
24
+ ],
25
+ dealTabRoutes: [...],
26
+ individualTabRoutes: [...],
27
+ })
28
+ ```
29
+
30
+ ### Custom sidebar sections
31
+
32
+ Accordion sections rendered at the end of the sidebar's Overview tab, after the built-in sections. They show in both the detail view and the quick-view dialog. Register them through the `business` / `individual` options of `useWiseCrmConfig().init()`:
33
+
34
+ ```ts
35
+ import type { WiseCrmSidebarSection } from '@wisemen/wise-crm-web'
36
+
37
+ useWiseCrmConfig().init({
38
+ // ...
39
+ options: {
40
+ business: {
41
+ sidebarSections: [
42
+ {
43
+ icon: ShoppingCart01Icon,
44
+ key: 'orders',
45
+ label: computed<string>(() => t('crm.business.sidebar.orders')),
46
+ component: defineAsyncComponent(() => import('./BusinessOrdersSidebarSection.vue')),
47
+ },
48
+ ],
49
+ },
50
+ individual: {
51
+ sidebarSections: [...],
52
+ },
53
+ // ...
54
+ },
55
+ })
56
+ ```
57
+
58
+ | Field | Description |
59
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `key` | Unique per entity; used as the accordion item value. |
61
+ | `label` | Section header, a `ComputedRef<string>` so it follows the locale. |
62
+ | `icon` | Section header icon. |
63
+ | `component` | Section body. |
64
+ | `useIsVisible` | Optional. Receives the component's props as a `ComputedRef` and returns a `ComputedRef<boolean>`; the section is hidden while it is `false`. |
65
+
66
+ The component receives only the entity uuid. It fetches its own data and renders its own loading and empty state. Sections are open by default.
67
+
68
+ ```vue
69
+ <script setup lang="ts">
70
+ import type { WiseCrmBusinessUuid } from '@wisemen/wise-crm-web'
71
+
72
+ const props = defineProps<{
73
+ businessUuid: WiseCrmBusinessUuid
74
+ }>()
75
+ </script>
76
+ ```
77
+
78
+ Individual sections receive `individualUuid: WiseCrmIndividualUuid` instead. A component with the wrong props is a type error.
79
+
80
+ `useIsVisible` is called once in the sidebar's setup, so it can use composables such as queries. Use it to hide a section that has nothing to show for this entity, rather than rendering an empty body under its header:
81
+
82
+ ```ts
83
+ {
84
+ key: 'parent',
85
+ // ...
86
+ useIsVisible: (props) => useBusinessHasParent(computed(() => props.value.businessUuid)),
87
+ }
88
+ ```
89
+
90
+ A section that becomes visible after the sidebar has mounted still opens by default.
@@ -1,8 +1,8 @@
1
1
  import { defineComponent as n, openBlock as o, createBlock as r, unref as s, withCtx as a, createVNode as u } from "vue";
2
2
  import { useRouteParams as m } from "@vueuse/router";
3
- import { _ as p } from "./index-uh3Ew2Yy.js";
4
- import { aV as c } from "./uuid.util-B9bkU4IX.js";
5
- import { _ } from "./CrmDetailView.vue_vue_type_script_setup_true_lang-BoCwerkk.js";
3
+ import { _ as p } from "./index-BM5Kw4_U.js";
4
+ import { br as c } from "./uuid.util-SYJY4Bt5.js";
5
+ import { _ } from "./CrmDetailView.vue_vue_type_script_setup_true_lang-Cnn_HfDr.js";
6
6
  const x = /* @__PURE__ */ n({
7
7
  __name: "BusinessDetailView",
8
8
  props: {
@@ -1,9 +1,9 @@
1
- import { defineComponent as v, ref as h, unref as e, openBlock as d, createElementBlock as u, createElementVNode as t, renderSlot as x, createVNode as r, withCtx as f, Fragment as y, renderList as b, createBlock as g, toValue as m, createCommentVNode as w } from "vue";
1
+ import { defineComponent as v, ref as h, unref as e, openBlock as d, createElementBlock as u, createElementVNode as t, renderSlot as x, createVNode as r, withCtx as f, Fragment as y, renderList as g, createBlock as w, toValue as m, createCommentVNode as b } from "vue";
2
2
  import { onKeyStroke as C } from "@vueuse/core";
3
3
  import { UITabsRouterLink as k, UITabsRouterLinkItem as R, UIIconButton as B } from "@wisemen/vue-core-design-system";
4
- import { a1 as E, V as I, X as V, ab as T } from "./index-uh3Ew2Yy.js";
5
- import { useRoute as A, RouterView as H } from "vue-router";
6
- class U {
4
+ import { h as E, j as I, $ as T, ac as V } from "./index-BM5Kw4_U.js";
5
+ import { useRoute as $, RouterView as j } from "vue-router";
6
+ class A {
7
7
  /**
8
8
  * Checks if any parent element has `aria-hidden="true"` set. use this to check if a dialog or a popover is open.
9
9
  * @param element The element to start checking from.
@@ -19,23 +19,23 @@ class U {
19
19
  return !1;
20
20
  }
21
21
  }
22
- const $ = { key: 0 }, j = { class: "flex h-dvh w-dvw flex-1 overflow-hidden" }, z = { class: "flex h-full min-w-100 flex-col" }, L = { class: "flex size-full flex-1 justify-center overflow-hidden bg-secondary" }, N = { class: "flex w-full flex-col" }, S = { class: "grid w-full grid-cols-3 p-lg" }, X = { class: "flex items-center justify-center" }, D = { class: "flex items-center justify-end" }, F = { class: "size-full overflow-y-auto" }, O = /* @__PURE__ */ v({
22
+ const H = { key: 0 }, U = { class: "flex h-dvh w-dvw flex-1 overflow-hidden" }, z = { class: "flex h-full min-w-100 flex-col" }, L = { class: "flex size-full flex-1 justify-center overflow-hidden bg-secondary" }, N = { class: "flex w-full flex-col" }, S = { class: "grid w-full grid-cols-3 p-lg" }, D = { class: "flex items-center justify-center" }, F = { class: "flex items-center justify-end" }, K = { class: "size-full overflow-y-auto" }, P = /* @__PURE__ */ v({
23
23
  __name: "CrmDetailView",
24
24
  props: {
25
25
  entityType: {},
26
26
  tabs: {}
27
27
  },
28
28
  setup(i) {
29
- const a = i, o = h(null), c = E(), _ = I(), p = A();
29
+ const a = i, o = h(null), c = E(), _ = I(), p = $();
30
30
  return C("Escape", () => {
31
31
  const l = o.value?.$el;
32
- l && U.parentHasAriaHidden(l) || c.goToReturnRoute();
33
- }), (l, n) => e(_).isInitialized.value ? (d(), u("div", $, [
34
- t("div", j, [
32
+ l && A.parentHasAriaHidden(l) || c.goToReturnRoute();
33
+ }), (l, n) => e(_).isInitialized.value ? (d(), u("div", H, [
34
+ t("div", U, [
35
35
  t("div", z, [
36
36
  x(l.$slots, "sidebar")
37
37
  ]),
38
- r(V, {
38
+ r(T, {
39
39
  color: e(c).getEntityConfig(i.entityType)?.color ?? null,
40
40
  class: "relative flex flex-1 flex-col overflow-hidden"
41
41
  }, {
@@ -44,10 +44,10 @@ const $ = { key: 0 }, j = { class: "flex h-dvh w-dvw flex-1 overflow-hidden" },
44
44
  t("div", N, [
45
45
  t("div", S, [
46
46
  n[1] || (n[1] = t("div", null, null, -1)),
47
- t("div", X, [
47
+ t("div", D, [
48
48
  r(e(k), { variant: "button-border" }, {
49
49
  default: f(() => [
50
- (d(!0), u(y, null, b(a.tabs, (s) => (d(), g(e(R), {
50
+ (d(!0), u(y, null, g(a.tabs, (s) => (d(), w(e(R), {
51
51
  key: m(s.label),
52
52
  label: m(s.label),
53
53
  to: {
@@ -59,22 +59,22 @@ const $ = { key: 0 }, j = { class: "flex h-dvh w-dvw flex-1 overflow-hidden" },
59
59
  _: 1
60
60
  })
61
61
  ]),
62
- t("div", D, [
62
+ t("div", F, [
63
63
  r(e(B), {
64
64
  ref_key: "closeButtonRef",
65
65
  ref: o,
66
66
  "class-config": {
67
67
  root: "rounded-md shadow-lg"
68
68
  },
69
- icon: e(T),
69
+ icon: e(V),
70
70
  label: "Close",
71
71
  variant: "secondary",
72
72
  onClick: n[0] || (n[0] = (s) => e(c).goToReturnRoute())
73
73
  }, null, 8, ["icon"])
74
74
  ])
75
75
  ]),
76
- t("div", F, [
77
- r(e(H))
76
+ t("div", K, [
77
+ r(e(j))
78
78
  ])
79
79
  ])
80
80
  ])
@@ -82,9 +82,9 @@ const $ = { key: 0 }, j = { class: "flex h-dvh w-dvw flex-1 overflow-hidden" },
82
82
  _: 1
83
83
  }, 8, ["color"])
84
84
  ])
85
- ])) : w("", !0);
85
+ ])) : b("", !0);
86
86
  }
87
87
  });
88
88
  export {
89
- O as _
89
+ P as _
90
90
  };
@@ -0,0 +1,265 @@
1
+ import { defineComponent as y, openBlock as t, createElementBlock as _, createElementVNode as s, createBlock as o, resolveDynamicComponent as x, unref as e, toDisplayString as p, createVNode as r, withCtx as n, createCommentVNode as f, Fragment as b, renderList as w, normalizeClass as k, computed as I } from "vue";
2
+ import { useRouteParams as U } from "@vueuse/router";
3
+ import { useOffsetInfiniteQuery as M } from "@wisemen/vue-core-api-utils";
4
+ import { M as z, u as E, a as L, T as j, b as B, W as F, F as T, c as S, D as h, g as A, d as D, m as N, e as V, A as H } from "./index-BM5Kw4_U.js";
5
+ import { UIIconButton as Q, UIEmptyState as q, UIBadge as P } from "@wisemen/vue-core-design-system";
6
+ import { AnimatePresence as W, Motion as O } from "motion-v";
7
+ import { AccordionRoot as R, AccordionItem as G, AccordionHeader as J, AccordionTrigger as K, AccordionContent as X } from "reka-ui";
8
+ import { useI18n as Y } from "vue-i18n";
9
+ function Z(u) {
10
+ return M("mailIndex", {
11
+ params: {
12
+ queryParams: u.params
13
+ },
14
+ queryFn: (a) => z.getAll({
15
+ filters: u.params.filters.value,
16
+ pagination: a
17
+ })
18
+ });
19
+ }
20
+ const ee = { class: "group flex w-fit items-center justify-between gap-sm rounded-lg bg-secondary px-xs py-xxs text-sm font-medium" }, te = { class: "flex items-center gap-sm" }, se = {
21
+ key: 1,
22
+ "aria-hidden": !0,
23
+ class: "flex size-8 shrink-0 items-center justify-center"
24
+ }, ae = { class: "truncate text-sm text-primary" }, ne = { class: "flex items-center gap-md" }, le = /* @__PURE__ */ y({
25
+ __name: "DealExternalAttachment",
26
+ props: {
27
+ attachment: {}
28
+ },
29
+ setup(u) {
30
+ const a = u, d = E();
31
+ async function m(c) {
32
+ if (c.downloadUrl !== null) {
33
+ T.download(
34
+ c.downloadUrl,
35
+ c.name
36
+ );
37
+ return;
38
+ }
39
+ const l = await S.download(c.uuid);
40
+ if (l.isErr()) {
41
+ d.show(l.error);
42
+ return;
43
+ }
44
+ T.downloadBlob(l.value, c.name);
45
+ }
46
+ return (c, l) => (t(), _("div", ee, [
47
+ s("div", te, [
48
+ a.attachment.downloadUrl !== null && a.attachment.mimeType.startsWith("image/") ? (t(), o(L, {
49
+ key: 0,
50
+ "is-zoom-enabled": !0,
51
+ alt: a.attachment.name,
52
+ src: a.attachment.downloadUrl,
53
+ blurhash: a.attachment.blurHash ?? null,
54
+ class: "size-8 shrink-0 rounded-md",
55
+ fit: "cover"
56
+ }, null, 8, ["alt", "src", "blurhash"])) : (t(), _("div", se, [
57
+ (t(), o(x(e(j)), { class: "size-4.5 text-disabled" }))
58
+ ])),
59
+ s("p", ae, p(a.attachment.name), 1)
60
+ ]),
61
+ s("div", ne, [
62
+ r(B, null, {
63
+ default: n(() => [
64
+ r(e(Q), {
65
+ icon: e(F),
66
+ label: "Download",
67
+ variant: "tertiary",
68
+ class: "shrink-0 hover:bg-tertiary!",
69
+ onClick: l[0] || (l[0] = (C) => m(u.attachment))
70
+ }, null, 8, ["icon"])
71
+ ]),
72
+ _: 1
73
+ })
74
+ ])
75
+ ]));
76
+ }
77
+ }), oe = { class: "p-xl" }, ie = { class: "overflow-hidden rounded-xl border border-secondary bg-primary shadow-lg" }, re = { class: "text-md font-medium" }, ce = { class: "relative" }, ue = { class: "sr-only" }, de = { class: "flex min-w-0 flex-1 items-center gap-sm" }, me = { class: "truncate" }, _e = { class: "pointer-events-none flex items-center gap-sm" }, pe = { class: "flex flex-col items-end text-sm text-tertiary" }, fe = { class: "flex flex-col" }, he = ["innerHTML"], xe = /* @__PURE__ */ y({
78
+ __name: "DealCommunicationMailList",
79
+ props: {
80
+ mails: {}
81
+ },
82
+ setup(u) {
83
+ const a = u, d = Y();
84
+ function m(l) {
85
+ return V().toDateTime(l.createdAt);
86
+ }
87
+ function c(l) {
88
+ return l.to.join(", ");
89
+ }
90
+ return (l, C) => (t(), _("div", oe, [
91
+ s("div", ie, [
92
+ r(h, null, {
93
+ default: n(() => [
94
+ s("p", re, p(e(d).t("wise_crm.deal.communication.mail_list.title")), 1)
95
+ ]),
96
+ _: 1
97
+ }),
98
+ a.mails.length === 0 ? (t(), o(e(q), {
99
+ key: 0,
100
+ icon: e(A),
101
+ title: e(d).t("wise_crm.deal.communication.mail_list.no_communication_sent"),
102
+ description: e(d).t("wise_crm.deal.communication.mail_list.create_quote_instruction"),
103
+ class: "h-120"
104
+ }, null, 8, ["icon", "title", "description"])) : f("", !0),
105
+ a.mails.length > 0 ? (t(), o(e(R), {
106
+ key: 1,
107
+ collapsible: !0,
108
+ type: "multiple",
109
+ class: "flex flex-col gap-0"
110
+ }, {
111
+ default: n(() => [
112
+ (t(!0), _(b, null, w(a.mails, (i, $) => (t(), o(e(G), {
113
+ key: i.uuid,
114
+ value: i.uuid
115
+ }, {
116
+ default: n(({ open: g }) => [
117
+ r(e(J), null, {
118
+ default: n(() => [
119
+ s("div", ce, [
120
+ r(e(K), { class: "absolute inset-0 rounded-none ring-brand-500 ring-offset-1 duration-200 outline-none focus-visible:ring-2" }, {
121
+ default: n(() => [
122
+ s("span", ue, p(i.subject), 1)
123
+ ]),
124
+ _: 2
125
+ }, 1024),
126
+ s("div", {
127
+ class: k([{
128
+ "border-b border-secondary": $ < a.mails.length - 1
129
+ }, "px-2xl py-lg"])
130
+ }, [
131
+ r(D, { justify: "between" }, {
132
+ default: n(() => [
133
+ s("div", de, [
134
+ (t(), o(x(e(A)), { class: "size-4 shrink-0 text-secondary" })),
135
+ s("p", me, p(i.subject), 1),
136
+ i.mailTemplateDisplayName !== null ? (t(), o(e(P), {
137
+ key: 0,
138
+ label: i.mailTemplateDisplayName,
139
+ "class-config": {
140
+ root: "shrink-0"
141
+ },
142
+ variant: "translucent",
143
+ color: "brand"
144
+ }, null, 8, ["label"])) : f("", !0)
145
+ ]),
146
+ s("div", _e, [
147
+ s("div", pe, [
148
+ s("span", null, p(c(i)), 1),
149
+ s("span", null, p(m(i)), 1)
150
+ ]),
151
+ (t(), o(x(e(N)), {
152
+ class: k([{
153
+ "-rotate-180": g
154
+ }, "size-4 shrink-0 text-secondary duration-300"])
155
+ }, null, 8, ["class"]))
156
+ ])
157
+ ]),
158
+ _: 2
159
+ }, 1024)
160
+ ], 2)
161
+ ])
162
+ ]),
163
+ _: 2
164
+ }, 1024),
165
+ r(e(W), { initial: !1 }, {
166
+ default: n(() => [
167
+ g ? (t(), o(e(X), {
168
+ key: 0,
169
+ class: "overflow-clip"
170
+ }, {
171
+ default: n(() => [
172
+ r(e(O), {
173
+ initial: {
174
+ opacity: 0,
175
+ height: 0,
176
+ y: -10
177
+ },
178
+ animate: {
179
+ opacity: 1,
180
+ height: "auto",
181
+ filter: "blur(0px)",
182
+ y: 0
183
+ },
184
+ exit: {
185
+ opacity: 0,
186
+ height: 0,
187
+ filter: "blur(5px)",
188
+ y: -10
189
+ },
190
+ transition: {
191
+ bounce: 0,
192
+ duration: 0.4,
193
+ type: "spring"
194
+ }
195
+ }, {
196
+ default: n(() => [
197
+ s("div", fe, [
198
+ i.attachments.length > 0 ? (t(), o(h, { key: 0 }, {
199
+ default: n(() => [
200
+ r(D, { class: "flex-wrap" }, {
201
+ default: n(() => [
202
+ (t(!0), _(b, null, w(i.attachments, (v) => (t(), o(le, {
203
+ key: v.uuid,
204
+ attachment: v
205
+ }, null, 8, ["attachment"]))), 128))
206
+ ]),
207
+ _: 2
208
+ }, 1024)
209
+ ]),
210
+ _: 2
211
+ }, 1024)) : f("", !0),
212
+ r(h, null, {
213
+ default: n(() => [
214
+ s("div", {
215
+ class: "prose prose-sm max-w-full py-3xl pb-xl",
216
+ innerHTML: i.text
217
+ }, null, 8, he)
218
+ ]),
219
+ _: 2
220
+ }, 1024)
221
+ ])
222
+ ]),
223
+ _: 2
224
+ }, 1024)
225
+ ]),
226
+ _: 2
227
+ }, 1024)) : f("", !0)
228
+ ]),
229
+ _: 2
230
+ }, 1024)
231
+ ]),
232
+ _: 2
233
+ }, 1032, ["value"]))), 128))
234
+ ]),
235
+ _: 1
236
+ })) : f("", !0)
237
+ ])
238
+ ]));
239
+ }
240
+ }), ye = {
241
+ key: 0,
242
+ class: "p-xl"
243
+ }, Ce = /* @__PURE__ */ y({
244
+ __name: "DealCommunicationTabView",
245
+ setup(u) {
246
+ const a = U("dealUuid"), d = I(() => ({
247
+ dealUuid: a.value
248
+ })), m = Z({
249
+ params: {
250
+ filters: d
251
+ }
252
+ });
253
+ return (c, l) => (t(), _("div", null, [
254
+ e(m).isLoading.value ? (t(), _("div", ye, [
255
+ r(H, { class: "h-150 w-full" })
256
+ ])) : e(m).result.value.isOk() ? (t(), o(xe, {
257
+ key: 1,
258
+ mails: e(m).result.value.getValue().data
259
+ }, null, 8, ["mails"])) : f("", !0)
260
+ ]));
261
+ }
262
+ });
263
+ export {
264
+ Ce as default
265
+ };