@consilioweb/payload-support 0.6.4 → 0.7.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/README.md CHANGED
@@ -37,7 +37,7 @@
37
37
  >
38
38
  > And ensure all `@consilioweb/*` packages are in `transpilePackages` in your `next.config.ts`:
39
39
  > ```ts
40
- > transpilePackages: ['@consilioweb/seo-analyzer', '@consilioweb/admin-nav', /* ...other @consilioweb packages */],
40
+ > transpilePackages: ['@consilioweb/payload-seo-analyzer', '@consilioweb/payload-admin-nav', /* ...other @consilioweb packages */],
41
41
  > ```
42
42
  >
43
43
  > ✅ **Next.js 15** works without any workaround.
@@ -75,7 +75,7 @@ function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setRepl
75
75
  }
76
76
  setAiReplying(false);
77
77
  };
78
- const handleAiRewrite = async () => {
78
+ const handleAiRewrite = async (style = "auto") => {
79
79
  if (!replyBody.trim()) return;
80
80
  setAiRewriting(true);
81
81
  try {
@@ -83,7 +83,7 @@ function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setRepl
83
83
  method: "POST",
84
84
  headers: { "Content-Type": "application/json" },
85
85
  credentials: "include",
86
- body: JSON.stringify({ action: "rewrite", text: replyBody })
86
+ body: JSON.stringify({ action: "rewrite", text: replyBody, style })
87
87
  });
88
88
  if (res.ok) {
89
89
  const data = await res.json();
@@ -74,7 +74,7 @@ function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setRepl
74
74
  }
75
75
  setAiReplying(false);
76
76
  };
77
- const handleAiRewrite = async () => {
77
+ const handleAiRewrite = async (style = "auto") => {
78
78
  if (!replyBody.trim()) return;
79
79
  setAiRewriting(true);
80
80
  try {
@@ -82,7 +82,7 @@ function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setRepl
82
82
  method: "POST",
83
83
  headers: { "Content-Type": "application/json" },
84
84
  credentials: "include",
85
- body: JSON.stringify({ action: "rewrite", text: replyBody })
85
+ body: JSON.stringify({ action: "rewrite", text: replyBody, style })
86
86
  });
87
87
  if (res.ok) {
88
88
  const data = await res.json();
@@ -72,6 +72,100 @@ const layoutStyles = {
72
72
  top: "80px"
73
73
  }
74
74
  };
75
+ const REWRITE_STYLES = [
76
+ { id: "auto", label: "\u270F\uFE0F Auto", desc: "Garde le ton actuel" },
77
+ { id: "tutoyer", label: "\u{1F44B} Tutoyer", desc: "Passe en tu" },
78
+ { id: "vouvoyer", label: "\u{1F3A9} Vouvoyer", desc: "Passe en vous" },
79
+ { id: "formel", label: "\u{1F4BC} Formel", desc: "Ton professionnel" },
80
+ { id: "amical", label: "\u{1F60A} Amical", desc: "Ton chaleureux" },
81
+ { id: "court", label: "\u26A1 Court", desc: "Version concise" }
82
+ ];
83
+ const RewriteDropdown = ({ disabled, loading, onSelect }) => {
84
+ const [open, setOpen] = React.useState(false);
85
+ const ref = React__default.default.useRef(null);
86
+ React__default.default.useEffect(() => {
87
+ if (!open) return;
88
+ const close = (e) => {
89
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
90
+ };
91
+ document.addEventListener("mousedown", close);
92
+ return () => document.removeEventListener("mousedown", close);
93
+ }, [open]);
94
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, style: { position: "relative", display: "inline-block" }, children: [
95
+ /* @__PURE__ */ jsxRuntime.jsxs(
96
+ "button",
97
+ {
98
+ type: "button",
99
+ onClick: () => setOpen(!open),
100
+ disabled,
101
+ style: {
102
+ ...constants.s.outlineBtn("#0891b2", disabled),
103
+ fontSize: "11px",
104
+ padding: "3px 10px",
105
+ borderRadius: "14px",
106
+ display: "flex",
107
+ alignItems: "center",
108
+ gap: "4px"
109
+ },
110
+ children: [
111
+ loading ? "Reformulation..." : "\u270F\uFE0F Reformuler",
112
+ !loading && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 9, opacity: 0.7 }, children: "\u25BC" })
113
+ ]
114
+ }
115
+ ),
116
+ open && !disabled && !loading && /* @__PURE__ */ jsxRuntime.jsx(
117
+ "div",
118
+ {
119
+ style: {
120
+ position: "absolute",
121
+ bottom: "100%",
122
+ left: 0,
123
+ marginBottom: 4,
124
+ background: "var(--theme-elevation-0, #fff)",
125
+ border: "1px solid var(--theme-elevation-200, #e5e7eb)",
126
+ borderRadius: 8,
127
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
128
+ zIndex: 100,
129
+ minWidth: 180,
130
+ overflow: "hidden"
131
+ },
132
+ children: REWRITE_STYLES.map((style) => /* @__PURE__ */ jsxRuntime.jsxs(
133
+ "button",
134
+ {
135
+ type: "button",
136
+ onClick: () => {
137
+ setOpen(false);
138
+ onSelect(style.id);
139
+ },
140
+ style: {
141
+ display: "flex",
142
+ flexDirection: "column",
143
+ width: "100%",
144
+ padding: "8px 12px",
145
+ border: "none",
146
+ background: "transparent",
147
+ cursor: "pointer",
148
+ textAlign: "left",
149
+ borderBottom: "1px solid var(--theme-elevation-100, #f3f4f6)",
150
+ transition: "background 120ms"
151
+ },
152
+ onMouseEnter: (e) => {
153
+ e.target.style.background = "var(--theme-elevation-50, #f9fafb)";
154
+ },
155
+ onMouseLeave: (e) => {
156
+ e.target.style.background = "transparent";
157
+ },
158
+ children: [
159
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12, fontWeight: 600, color: "var(--theme-text, #111)" }, children: style.label }),
160
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 10, color: "var(--theme-elevation-500, #6b7280)" }, children: style.desc })
161
+ ]
162
+ },
163
+ style.id
164
+ ))
165
+ }
166
+ )
167
+ ] });
168
+ };
75
169
  const TicketConversation = () => {
76
170
  const { id } = useDocumentIdFromUrl();
77
171
  const [features] = React.useState(() => config.getFeatures());
@@ -805,12 +899,11 @@ const TicketConversation = () => {
805
899
  }
806
900
  ),
807
901
  features.ai && /* @__PURE__ */ jsxRuntime.jsx(
808
- "button",
902
+ RewriteDropdown,
809
903
  {
810
- onClick: handleAiRewrite,
811
904
  disabled: aiRewriting || !replyBody.trim(),
812
- style: { ...constants.s.outlineBtn("#0891b2", aiRewriting || !replyBody.trim()), fontSize: "11px", padding: "3px 10px", borderRadius: "14px" },
813
- children: aiRewriting ? "Reformulation..." : "Reformuler"
905
+ loading: aiRewriting,
906
+ onSelect: (style) => handleAiRewrite(style)
814
907
  }
815
908
  ),
816
909
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -67,6 +67,100 @@ const layoutStyles = {
67
67
  top: "80px"
68
68
  }
69
69
  };
70
+ const REWRITE_STYLES = [
71
+ { id: "auto", label: "\u270F\uFE0F Auto", desc: "Garde le ton actuel" },
72
+ { id: "tutoyer", label: "\u{1F44B} Tutoyer", desc: "Passe en tu" },
73
+ { id: "vouvoyer", label: "\u{1F3A9} Vouvoyer", desc: "Passe en vous" },
74
+ { id: "formel", label: "\u{1F4BC} Formel", desc: "Ton professionnel" },
75
+ { id: "amical", label: "\u{1F60A} Amical", desc: "Ton chaleureux" },
76
+ { id: "court", label: "\u26A1 Court", desc: "Version concise" }
77
+ ];
78
+ const RewriteDropdown = ({ disabled, loading, onSelect }) => {
79
+ const [open, setOpen] = useState(false);
80
+ const ref = React.useRef(null);
81
+ React.useEffect(() => {
82
+ if (!open) return;
83
+ const close = (e) => {
84
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
85
+ };
86
+ document.addEventListener("mousedown", close);
87
+ return () => document.removeEventListener("mousedown", close);
88
+ }, [open]);
89
+ return /* @__PURE__ */ jsxs("div", { ref, style: { position: "relative", display: "inline-block" }, children: [
90
+ /* @__PURE__ */ jsxs(
91
+ "button",
92
+ {
93
+ type: "button",
94
+ onClick: () => setOpen(!open),
95
+ disabled,
96
+ style: {
97
+ ...s.outlineBtn("#0891b2", disabled),
98
+ fontSize: "11px",
99
+ padding: "3px 10px",
100
+ borderRadius: "14px",
101
+ display: "flex",
102
+ alignItems: "center",
103
+ gap: "4px"
104
+ },
105
+ children: [
106
+ loading ? "Reformulation..." : "\u270F\uFE0F Reformuler",
107
+ !loading && /* @__PURE__ */ jsx("span", { style: { fontSize: 9, opacity: 0.7 }, children: "\u25BC" })
108
+ ]
109
+ }
110
+ ),
111
+ open && !disabled && !loading && /* @__PURE__ */ jsx(
112
+ "div",
113
+ {
114
+ style: {
115
+ position: "absolute",
116
+ bottom: "100%",
117
+ left: 0,
118
+ marginBottom: 4,
119
+ background: "var(--theme-elevation-0, #fff)",
120
+ border: "1px solid var(--theme-elevation-200, #e5e7eb)",
121
+ borderRadius: 8,
122
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
123
+ zIndex: 100,
124
+ minWidth: 180,
125
+ overflow: "hidden"
126
+ },
127
+ children: REWRITE_STYLES.map((style) => /* @__PURE__ */ jsxs(
128
+ "button",
129
+ {
130
+ type: "button",
131
+ onClick: () => {
132
+ setOpen(false);
133
+ onSelect(style.id);
134
+ },
135
+ style: {
136
+ display: "flex",
137
+ flexDirection: "column",
138
+ width: "100%",
139
+ padding: "8px 12px",
140
+ border: "none",
141
+ background: "transparent",
142
+ cursor: "pointer",
143
+ textAlign: "left",
144
+ borderBottom: "1px solid var(--theme-elevation-100, #f3f4f6)",
145
+ transition: "background 120ms"
146
+ },
147
+ onMouseEnter: (e) => {
148
+ e.target.style.background = "var(--theme-elevation-50, #f9fafb)";
149
+ },
150
+ onMouseLeave: (e) => {
151
+ e.target.style.background = "transparent";
152
+ },
153
+ children: [
154
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12, fontWeight: 600, color: "var(--theme-text, #111)" }, children: style.label }),
155
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "var(--theme-elevation-500, #6b7280)" }, children: style.desc })
156
+ ]
157
+ },
158
+ style.id
159
+ ))
160
+ }
161
+ )
162
+ ] });
163
+ };
70
164
  const TicketConversation = () => {
71
165
  const { id } = useDocumentIdFromUrl();
72
166
  const [features] = useState(() => getFeatures());
@@ -800,12 +894,11 @@ const TicketConversation = () => {
800
894
  }
801
895
  ),
802
896
  features.ai && /* @__PURE__ */ jsx(
803
- "button",
897
+ RewriteDropdown,
804
898
  {
805
- onClick: handleAiRewrite,
806
899
  disabled: aiRewriting || !replyBody.trim(),
807
- style: { ...s.outlineBtn("#0891b2", aiRewriting || !replyBody.trim()), fontSize: "11px", padding: "3px 10px", borderRadius: "14px" },
808
- children: aiRewriting ? "Reformulation..." : "Reformuler"
900
+ loading: aiRewriting,
901
+ onSelect: (style) => handleAiRewrite(style)
809
902
  }
810
903
  ),
811
904
  /* @__PURE__ */ jsx(
package/dist/index.cjs CHANGED
@@ -280,9 +280,18 @@ R\xE9dige une r\xE9ponse appropri\xE9e au dernier message du client. Sois concis
280
280
  if (!aiSettings.enableRewrite) {
281
281
  return Response.json({ rewritten: "", disabled: true });
282
282
  }
283
- const { text } = body;
283
+ const { text, style } = body;
284
284
  if (!text?.trim()) return Response.json({ error: "text required" }, { status: 400 });
285
- const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de mani\xE8re plus professionnelle et corrige les fautes d'orthographe/grammaire. Garde le m\xEAme sens et le m\xEAme ton (tutoiement/vouvoiement). Ne change pas le fond du message, am\xE9liore uniquement la forme. R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
285
+ const styleInstructions = {
286
+ auto: "Garde le m\xEAme ton (tutoiement/vouvoiement).",
287
+ tutoyer: "Utilise le tutoiement. Si le texte vouvoie, convertis en tutoiement.",
288
+ vouvoyer: "Utilise le vouvoiement. Si le texte tutoie, convertis en vouvoiement.",
289
+ formel: "Adopte un ton formel et professionnel avec vouvoiement.",
290
+ court: "Raccourcis le texte au maximum tout en gardant le sens. Sois concis et direct.",
291
+ amical: "Adopte un ton chaleureux et amical avec tutoiement."
292
+ };
293
+ const styleGuide = styleInstructions[style || "auto"] || styleInstructions.auto;
294
+ const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de mani\xE8re plus professionnelle et corrige les fautes d'orthographe/grammaire. ${styleGuide} Ne change pas le fond du message, am\xE9liore uniquement la forme. R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
286
295
 
287
296
  Texte original :
288
297
  ${text}`;
@@ -5672,6 +5681,37 @@ function createTicketsCollection(slugs, options) {
5672
5681
  });
5673
5682
  }
5674
5683
  const billingFields = [
5684
+ {
5685
+ type: "row",
5686
+ fields: [
5687
+ {
5688
+ name: "billingType",
5689
+ type: "select",
5690
+ label: "Type de facturation",
5691
+ defaultValue: "hourly",
5692
+ options: [
5693
+ { label: "Au temps pass\xE9", value: "hourly" },
5694
+ { label: "Forfait", value: "flat" }
5695
+ ],
5696
+ admin: { width: "33%" }
5697
+ },
5698
+ {
5699
+ name: "flatRateAmount",
5700
+ type: "number",
5701
+ label: "Montant forfait (EUR)",
5702
+ admin: {
5703
+ width: "33%",
5704
+ condition: (data) => data?.billingType === "flat"
5705
+ }
5706
+ },
5707
+ {
5708
+ name: "billedAmount",
5709
+ type: "number",
5710
+ label: "Montant factur\xE9 (EUR)",
5711
+ admin: { width: "33%" }
5712
+ }
5713
+ ]
5714
+ },
5675
5715
  {
5676
5716
  type: "row",
5677
5717
  fields: [
@@ -5712,14 +5752,8 @@ function createTicketsCollection(slugs, options) {
5712
5752
  {
5713
5753
  name: "paidAt",
5714
5754
  type: "date",
5715
- label: "Paye le",
5755
+ label: "Pay\xE9 le",
5716
5756
  admin: { width: "33%", date: { displayFormat: "dd/MM/yyyy HH:mm" } }
5717
- },
5718
- {
5719
- name: "billedAmount",
5720
- type: "number",
5721
- label: "Montant facture (EUR)",
5722
- admin: { width: "33%" }
5723
5757
  }
5724
5758
  ]
5725
5759
  }
@@ -5829,7 +5863,7 @@ function createTicketsCollection(slugs, options) {
5829
5863
  ]
5830
5864
  },
5831
5865
  // Sidebar
5832
- { name: "ticketNumber", type: "text", unique: true, label: "N\xB0 Ticket", admin: { readOnly: true, position: "sidebar" } },
5866
+ { name: "ticketNumber", type: "text", unique: true, label: "N\xB0 Ticket", admin: { position: "sidebar" } },
5833
5867
  { name: "slaPolicy", type: "relationship", relationTo: slugs.slaPolicies, label: "Politique SLA", admin: { position: "sidebar" } },
5834
5868
  {
5835
5869
  name: "category",
@@ -5881,7 +5915,17 @@ function createTicketsCollection(slugs, options) {
5881
5915
  { name: "snoozeUntil", type: "date", label: "Snooze jusqu'au", admin: { position: "sidebar", date: { pickerAppearance: "dayAndTime", displayFormat: "dd/MM/yyyy HH:mm" } } },
5882
5916
  // Billing sidebar
5883
5917
  { name: "billable", type: "checkbox", defaultValue: true, label: "Facturable", admin: { position: "sidebar" } },
5884
- { name: "showTimeToClient", type: "checkbox", defaultValue: true, label: "Afficher le temps au client", admin: { position: "sidebar" } },
5918
+ {
5919
+ name: "showTimeToClient",
5920
+ type: "checkbox",
5921
+ defaultValue: true,
5922
+ label: "Afficher le temps au client",
5923
+ admin: {
5924
+ position: "sidebar",
5925
+ description: "Auto-d\xE9sactiv\xE9 en mode forfait",
5926
+ condition: (data) => data?.billingType !== "flat"
5927
+ }
5928
+ },
5885
5929
  { name: "totalTimeMinutes", type: "number", defaultValue: 0, label: "Temps total (minutes)", admin: { readOnly: true, position: "sidebar" } }
5886
5930
  ],
5887
5931
  hooks: {
package/dist/index.js CHANGED
@@ -274,9 +274,18 @@ R\xE9dige une r\xE9ponse appropri\xE9e au dernier message du client. Sois concis
274
274
  if (!aiSettings.enableRewrite) {
275
275
  return Response.json({ rewritten: "", disabled: true });
276
276
  }
277
- const { text } = body;
277
+ const { text, style } = body;
278
278
  if (!text?.trim()) return Response.json({ error: "text required" }, { status: 400 });
279
- const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de mani\xE8re plus professionnelle et corrige les fautes d'orthographe/grammaire. Garde le m\xEAme sens et le m\xEAme ton (tutoiement/vouvoiement). Ne change pas le fond du message, am\xE9liore uniquement la forme. R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
279
+ const styleInstructions = {
280
+ auto: "Garde le m\xEAme ton (tutoiement/vouvoiement).",
281
+ tutoyer: "Utilise le tutoiement. Si le texte vouvoie, convertis en tutoiement.",
282
+ vouvoyer: "Utilise le vouvoiement. Si le texte tutoie, convertis en vouvoiement.",
283
+ formel: "Adopte un ton formel et professionnel avec vouvoiement.",
284
+ court: "Raccourcis le texte au maximum tout en gardant le sens. Sois concis et direct.",
285
+ amical: "Adopte un ton chaleureux et amical avec tutoiement."
286
+ };
287
+ const styleGuide = styleInstructions[style || "auto"] || styleInstructions.auto;
288
+ const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de mani\xE8re plus professionnelle et corrige les fautes d'orthographe/grammaire. ${styleGuide} Ne change pas le fond du message, am\xE9liore uniquement la forme. R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
280
289
 
281
290
  Texte original :
282
291
  ${text}`;
@@ -5666,6 +5675,37 @@ function createTicketsCollection(slugs, options) {
5666
5675
  });
5667
5676
  }
5668
5677
  const billingFields = [
5678
+ {
5679
+ type: "row",
5680
+ fields: [
5681
+ {
5682
+ name: "billingType",
5683
+ type: "select",
5684
+ label: "Type de facturation",
5685
+ defaultValue: "hourly",
5686
+ options: [
5687
+ { label: "Au temps pass\xE9", value: "hourly" },
5688
+ { label: "Forfait", value: "flat" }
5689
+ ],
5690
+ admin: { width: "33%" }
5691
+ },
5692
+ {
5693
+ name: "flatRateAmount",
5694
+ type: "number",
5695
+ label: "Montant forfait (EUR)",
5696
+ admin: {
5697
+ width: "33%",
5698
+ condition: (data) => data?.billingType === "flat"
5699
+ }
5700
+ },
5701
+ {
5702
+ name: "billedAmount",
5703
+ type: "number",
5704
+ label: "Montant factur\xE9 (EUR)",
5705
+ admin: { width: "33%" }
5706
+ }
5707
+ ]
5708
+ },
5669
5709
  {
5670
5710
  type: "row",
5671
5711
  fields: [
@@ -5706,14 +5746,8 @@ function createTicketsCollection(slugs, options) {
5706
5746
  {
5707
5747
  name: "paidAt",
5708
5748
  type: "date",
5709
- label: "Paye le",
5749
+ label: "Pay\xE9 le",
5710
5750
  admin: { width: "33%", date: { displayFormat: "dd/MM/yyyy HH:mm" } }
5711
- },
5712
- {
5713
- name: "billedAmount",
5714
- type: "number",
5715
- label: "Montant facture (EUR)",
5716
- admin: { width: "33%" }
5717
5751
  }
5718
5752
  ]
5719
5753
  }
@@ -5823,7 +5857,7 @@ function createTicketsCollection(slugs, options) {
5823
5857
  ]
5824
5858
  },
5825
5859
  // Sidebar
5826
- { name: "ticketNumber", type: "text", unique: true, label: "N\xB0 Ticket", admin: { readOnly: true, position: "sidebar" } },
5860
+ { name: "ticketNumber", type: "text", unique: true, label: "N\xB0 Ticket", admin: { position: "sidebar" } },
5827
5861
  { name: "slaPolicy", type: "relationship", relationTo: slugs.slaPolicies, label: "Politique SLA", admin: { position: "sidebar" } },
5828
5862
  {
5829
5863
  name: "category",
@@ -5875,7 +5909,17 @@ function createTicketsCollection(slugs, options) {
5875
5909
  { name: "snoozeUntil", type: "date", label: "Snooze jusqu'au", admin: { position: "sidebar", date: { pickerAppearance: "dayAndTime", displayFormat: "dd/MM/yyyy HH:mm" } } },
5876
5910
  // Billing sidebar
5877
5911
  { name: "billable", type: "checkbox", defaultValue: true, label: "Facturable", admin: { position: "sidebar" } },
5878
- { name: "showTimeToClient", type: "checkbox", defaultValue: true, label: "Afficher le temps au client", admin: { position: "sidebar" } },
5912
+ {
5913
+ name: "showTimeToClient",
5914
+ type: "checkbox",
5915
+ defaultValue: true,
5916
+ label: "Afficher le temps au client",
5917
+ admin: {
5918
+ position: "sidebar",
5919
+ description: "Auto-d\xE9sactiv\xE9 en mode forfait",
5920
+ condition: (data) => data?.billingType !== "flat"
5921
+ }
5922
+ },
5879
5923
  { name: "totalTimeMinutes", type: "number", defaultValue: 0, label: "Temps total (minutes)", admin: { readOnly: true, position: "sidebar" } }
5880
5924
  ],
5881
5925
  hooks: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-support",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -458,6 +458,37 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
458
458
 
459
459
  // Billing collapsible fields
460
460
  const billingFields: Field[] = [
461
+ {
462
+ type: 'row',
463
+ fields: [
464
+ {
465
+ name: 'billingType',
466
+ type: 'select',
467
+ label: 'Type de facturation',
468
+ defaultValue: 'hourly',
469
+ options: [
470
+ { label: 'Au temps passé', value: 'hourly' },
471
+ { label: 'Forfait', value: 'flat' },
472
+ ],
473
+ admin: { width: '33%' },
474
+ },
475
+ {
476
+ name: 'flatRateAmount',
477
+ type: 'number',
478
+ label: 'Montant forfait (EUR)',
479
+ admin: {
480
+ width: '33%',
481
+ condition: (data) => data?.billingType === 'flat',
482
+ },
483
+ },
484
+ {
485
+ name: 'billedAmount',
486
+ type: 'number',
487
+ label: 'Montant facturé (EUR)',
488
+ admin: { width: '33%' },
489
+ },
490
+ ],
491
+ },
461
492
  {
462
493
  type: 'row',
463
494
  fields: [
@@ -498,15 +529,9 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
498
529
  {
499
530
  name: 'paidAt',
500
531
  type: 'date',
501
- label: 'Paye le',
532
+ label: 'Payé le',
502
533
  admin: { width: '33%', date: { displayFormat: 'dd/MM/yyyy HH:mm' } },
503
534
  },
504
- {
505
- name: 'billedAmount',
506
- type: 'number',
507
- label: 'Montant facture (EUR)',
508
- admin: { width: '33%' },
509
- },
510
535
  ],
511
536
  },
512
537
  ]
@@ -607,7 +632,7 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
607
632
  ],
608
633
  },
609
634
  // Sidebar
610
- { name: 'ticketNumber', type: 'text', unique: true, label: 'N° Ticket', admin: { readOnly: true, position: 'sidebar' } },
635
+ { name: 'ticketNumber', type: 'text', unique: true, label: 'N° Ticket', admin: { position: 'sidebar' } },
611
636
  { name: 'slaPolicy', type: 'relationship', relationTo: slugs.slaPolicies, label: 'Politique SLA', admin: { position: 'sidebar' } },
612
637
  {
613
638
  name: 'category', type: 'select', label: 'Categorie',
@@ -651,7 +676,17 @@ export function createTicketsCollection(slugs: CollectionSlugs, options?: {
651
676
  { name: 'snoozeUntil', type: 'date', label: 'Snooze jusqu\'au', admin: { position: 'sidebar', date: { pickerAppearance: 'dayAndTime', displayFormat: 'dd/MM/yyyy HH:mm' } } },
652
677
  // Billing sidebar
653
678
  { name: 'billable', type: 'checkbox', defaultValue: true, label: 'Facturable', admin: { position: 'sidebar' } },
654
- { name: 'showTimeToClient', type: 'checkbox', defaultValue: true, label: 'Afficher le temps au client', admin: { position: 'sidebar' } },
679
+ {
680
+ name: 'showTimeToClient',
681
+ type: 'checkbox',
682
+ defaultValue: true,
683
+ label: 'Afficher le temps au client',
684
+ admin: {
685
+ position: 'sidebar',
686
+ description: 'Auto-désactivé en mode forfait',
687
+ condition: (data) => data?.billingType !== 'flat',
688
+ },
689
+ },
655
690
  { name: 'totalTimeMinutes', type: 'number', defaultValue: 0, label: 'Temps total (minutes)', admin: { readOnly: true, position: 'sidebar' } },
656
691
  ],
657
692
  hooks: {
@@ -87,7 +87,7 @@ export function useAI(
87
87
  setAiReplying(false)
88
88
  }
89
89
 
90
- const handleAiRewrite = async () => {
90
+ const handleAiRewrite = async (style: string = 'auto') => {
91
91
  if (!replyBody.trim()) return
92
92
  setAiRewriting(true)
93
93
  try {
@@ -95,7 +95,7 @@ export function useAI(
95
95
  method: 'POST',
96
96
  headers: { 'Content-Type': 'application/json' },
97
97
  credentials: 'include',
98
- body: JSON.stringify({ action: 'rewrite', text: replyBody }),
98
+ body: JSON.stringify({ action: 'rewrite', text: replyBody, style }),
99
99
  })
100
100
  if (res.ok) {
101
101
  const data = await res.json()
@@ -82,6 +82,98 @@ const layoutStyles = {
82
82
  } as React.CSSProperties,
83
83
  }
84
84
 
85
+ // ── Rewrite style dropdown ──────────────────────────────────────────────
86
+ const REWRITE_STYLES = [
87
+ { id: 'auto', label: '✏️ Auto', desc: 'Garde le ton actuel' },
88
+ { id: 'tutoyer', label: '👋 Tutoyer', desc: 'Passe en tu' },
89
+ { id: 'vouvoyer', label: '🎩 Vouvoyer', desc: 'Passe en vous' },
90
+ { id: 'formel', label: '💼 Formel', desc: 'Ton professionnel' },
91
+ { id: 'amical', label: '😊 Amical', desc: 'Ton chaleureux' },
92
+ { id: 'court', label: '⚡ Court', desc: 'Version concise' },
93
+ ]
94
+
95
+ const RewriteDropdown: React.FC<{
96
+ disabled: boolean
97
+ loading: boolean
98
+ onSelect: (style: string) => void
99
+ }> = ({ disabled, loading, onSelect }) => {
100
+ const [open, setOpen] = useState(false)
101
+ const ref = React.useRef<HTMLDivElement>(null)
102
+
103
+ React.useEffect(() => {
104
+ if (!open) return
105
+ const close = (e: MouseEvent) => {
106
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
107
+ }
108
+ document.addEventListener('mousedown', close)
109
+ return () => document.removeEventListener('mousedown', close)
110
+ }, [open])
111
+
112
+ return (
113
+ <div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
114
+ <button
115
+ type="button"
116
+ onClick={() => setOpen(!open)}
117
+ disabled={disabled}
118
+ style={{
119
+ ...s.outlineBtn('#0891b2', disabled),
120
+ fontSize: '11px',
121
+ padding: '3px 10px',
122
+ borderRadius: '14px',
123
+ display: 'flex',
124
+ alignItems: 'center',
125
+ gap: '4px',
126
+ }}
127
+ >
128
+ {loading ? 'Reformulation...' : '✏️ Reformuler'}
129
+ {!loading && <span style={{ fontSize: 9, opacity: 0.7 }}>▼</span>}
130
+ </button>
131
+ {open && !disabled && !loading && (
132
+ <div
133
+ style={{
134
+ position: 'absolute',
135
+ bottom: '100%',
136
+ left: 0,
137
+ marginBottom: 4,
138
+ background: 'var(--theme-elevation-0, #fff)',
139
+ border: '1px solid var(--theme-elevation-200, #e5e7eb)',
140
+ borderRadius: 8,
141
+ boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
142
+ zIndex: 100,
143
+ minWidth: 180,
144
+ overflow: 'hidden',
145
+ }}
146
+ >
147
+ {REWRITE_STYLES.map((style) => (
148
+ <button
149
+ key={style.id}
150
+ type="button"
151
+ onClick={() => { setOpen(false); onSelect(style.id) }}
152
+ style={{
153
+ display: 'flex',
154
+ flexDirection: 'column',
155
+ width: '100%',
156
+ padding: '8px 12px',
157
+ border: 'none',
158
+ background: 'transparent',
159
+ cursor: 'pointer',
160
+ textAlign: 'left',
161
+ borderBottom: '1px solid var(--theme-elevation-100, #f3f4f6)',
162
+ transition: 'background 120ms',
163
+ }}
164
+ onMouseEnter={(e) => { (e.target as HTMLElement).style.background = 'var(--theme-elevation-50, #f9fafb)' }}
165
+ onMouseLeave={(e) => { (e.target as HTMLElement).style.background = 'transparent' }}
166
+ >
167
+ <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--theme-text, #111)' }}>{style.label}</span>
168
+ <span style={{ fontSize: 10, color: 'var(--theme-elevation-500, #6b7280)' }}>{style.desc}</span>
169
+ </button>
170
+ ))}
171
+ </div>
172
+ )}
173
+ </div>
174
+ )
175
+ }
176
+
85
177
  const TicketConversation: React.FC = () => {
86
178
  const { id } = useDocumentIdFromUrl()
87
179
  const [features] = useState<TicketingFeatures>(() => getFeatures())
@@ -836,13 +928,11 @@ const TicketConversation: React.FC = () => {
836
928
  </button>
837
929
  )}
838
930
  {features.ai && (
839
- <button
840
- onClick={handleAiRewrite}
931
+ <RewriteDropdown
841
932
  disabled={aiRewriting || !replyBody.trim()}
842
- style={{ ...s.outlineBtn('#0891b2', aiRewriting || !replyBody.trim()), fontSize: '11px', padding: '3px 10px', borderRadius: '14px' }}
843
- >
844
- {aiRewriting ? 'Reformulation...' : 'Reformuler'}
845
- </button>
933
+ loading={aiRewriting}
934
+ onSelect={(style) => handleAiRewrite(style)}
935
+ />
846
936
  )}
847
937
  <CodeBlockInserter
848
938
  style={{ ...s.outlineBtn('#059669', false), fontSize: '11px', padding: '3px 10px', borderRadius: '14px' }}
@@ -169,10 +169,20 @@ Rédige une réponse appropriée au dernier message du client. Sois concis (3-5
169
169
  if (!aiSettings.enableRewrite) {
170
170
  return Response.json({ rewritten: '', disabled: true })
171
171
  }
172
- const { text } = body as { text: string }
172
+ const { text, style } = body as { text: string; style?: string }
173
173
  if (!text?.trim()) return Response.json({ error: 'text required' }, { status: 400 })
174
174
 
175
- const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de manière plus professionnelle et corrige les fautes d'orthographe/grammaire. Garde le même sens et le même ton (tutoiement/vouvoiement). Ne change pas le fond du message, améliore uniquement la forme. Réponds UNIQUEMENT avec le texte reformulé, sans commentaire ni explication.
175
+ const styleInstructions: Record<string, string> = {
176
+ auto: 'Garde le même ton (tutoiement/vouvoiement).',
177
+ tutoyer: 'Utilise le tutoiement. Si le texte vouvoie, convertis en tutoiement.',
178
+ vouvoyer: 'Utilise le vouvoiement. Si le texte tutoie, convertis en vouvoiement.',
179
+ formel: 'Adopte un ton formel et professionnel avec vouvoiement.',
180
+ court: 'Raccourcis le texte au maximum tout en gardant le sens. Sois concis et direct.',
181
+ amical: 'Adopte un ton chaleureux et amical avec tutoiement.',
182
+ }
183
+ const styleGuide = styleInstructions[style || 'auto'] || styleInstructions.auto
184
+
185
+ const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de manière plus professionnelle et corrige les fautes d'orthographe/grammaire. ${styleGuide} Ne change pas le fond du message, améliore uniquement la forme. Réponds UNIQUEMENT avec le texte reformulé, sans commentaire ni explication.
176
186
 
177
187
  Texte original :
178
188
  ${text}`