@lv-x-software-house/x_view 1.2.5-dev.37 → 1.2.5-dev.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +335 -13
  2. package/dist/index.mjs +339 -15
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2851,7 +2851,7 @@ var userActionHandlers = {
2851
2851
  }
2852
2852
  },
2853
2853
  handleSaveNodeDetails: async (context, updatedNode, keepOpen = false) => {
2854
- var _a;
2854
+ var _a, _b, _c;
2855
2855
  const { graphDataRef, sceneDataRef, stateRef, setters } = context;
2856
2856
  if (!graphDataRef.current || !sceneDataRef.current) return;
2857
2857
  const {
@@ -2872,15 +2872,66 @@ var userActionHandlers = {
2872
2872
  const { parentFileId, ownerId } = parentInfo;
2873
2873
  const specificParentData = JSON.parse(JSON.stringify(graphDataRef.current[parentFileId]));
2874
2874
  const nodeIndex = specificParentData.nodes.findIndex((n) => String(n.id) === String(nodeToSave.id));
2875
- if (nodeIndex > -1) {
2876
- specificParentData.nodes[nodeIndex] = { ...nodeToSave };
2877
- } else {
2875
+ if (nodeIndex === -1) {
2878
2876
  console.error("Node n\xE3o encontrado nos dados do parent para atualiza\xE7\xE3o:", nodeToSave.id);
2879
2877
  alert("Erro interno: Node n\xE3o encontrado para salvar.");
2880
2878
  return;
2881
2879
  }
2880
+ const oldNode = specificParentData.nodes[nodeIndex];
2881
+ const isNewDone = oldNode.status !== "Done" && nodeToSave.status === "Done";
2882
+ specificParentData.nodes[nodeIndex] = { ...nodeToSave };
2883
+ if (isNewDone && nodeToSave.is_quest && ((_a = nodeToSave.recurrence_config) == null ? void 0 : _a.enabled)) {
2884
+ const nextDateObj = calculateNextRecurrenceDate(nodeToSave.recurrence_config);
2885
+ if (nextDateObj) {
2886
+ const currentCounter = sceneDataRef.current.quest_counter || 1;
2887
+ const nextCounter = currentCounter + 1;
2888
+ sceneDataRef.current.quest_counter = nextCounter;
2889
+ let nextName = nodeToSave.raw_title || nodeToSave.name;
2890
+ const prefixParts = (nodeToSave.name || "").split(" - \xBB ");
2891
+ if (prefixParts.length > 1) {
2892
+ const prefixBase = prefixParts[0].split(" - ")[0];
2893
+ nextName = `${prefixBase} - ${nextCounter} - \xBB ${nodeToSave.raw_title || prefixParts[1]}`;
2894
+ }
2895
+ const nextRecurrenceConfig = { ...nodeToSave.recurrence_config };
2896
+ if (nextRecurrenceConfig.type === "custom") {
2897
+ nextRecurrenceConfig.custom_date = "";
2898
+ }
2899
+ const nextQuestNode = {
2900
+ ...JSON.parse(JSON.stringify(nodeToSave)),
2901
+ // Deep copy properties
2902
+ id: import_short_uuid.default.generate(),
2903
+ name: nextName,
2904
+ status: "Backlog",
2905
+ color: "#64748b",
2906
+ // QUEST_STATUS_COLORS["Backlog"]
2907
+ assignee_id: nodeToSave.recurrence_config.next_assignee_id || "",
2908
+ Date: nextDateObj,
2909
+ timeline: [{
2910
+ id: import_short_uuid.default.generate(),
2911
+ type: "status_changed",
2912
+ timestamp: Date.now(),
2913
+ actorId: "system",
2914
+ description: `Quest criada automaticamente via recorr\xEAncia a partir da quest #${nodeToSave.id}`,
2915
+ links: [],
2916
+ images: [],
2917
+ metadata: { status: "Backlog" }
2918
+ }],
2919
+ version_node: {
2920
+ is_version: true,
2921
+ parent_node: ((_b = nodeToSave.version_node) == null ? void 0 : _b.parent_node) || nodeToSave.id
2922
+ },
2923
+ recurrence_config: nextRecurrenceConfig
2924
+ };
2925
+ specificParentData.nodes.push(nextQuestNode);
2926
+ if (stateRef.current.nodeObjects[String(nodeToSave.id)]) {
2927
+ const pos = stateRef.current.nodeObjects[String(nodeToSave.id)].position.clone();
2928
+ pos.x += 12;
2929
+ addStandaloneNodeToScene(stateRef.current, nextQuestNode, pos);
2930
+ }
2931
+ }
2932
+ }
2882
2933
  try {
2883
- const isView = ((_a = context.viewType) == null ? void 0 : _a.toLowerCase()) === "view";
2934
+ const isView = ((_c = context.viewType) == null ? void 0 : _c.toLowerCase()) === "view";
2884
2935
  if (isView && parentFileId === context.sceneConfigId) {
2885
2936
  const visualNodeIndex = sceneDataRef.current.nodes.findIndex((n) => String(n.id) === String(nodeToSave.id));
2886
2937
  if (visualNodeIndex > -1) {
@@ -3736,10 +3787,44 @@ function createTimelineEntry(actorId, description, type = "comment", extras = {}
3736
3787
  id: img.id || v4_default(),
3737
3788
  name: img.name || "",
3738
3789
  value: img.value || ""
3739
- })),
3740
- metadata
3790
+ }))
3741
3791
  };
3742
3792
  }
3793
+ function calculateNextRecurrenceDate(config) {
3794
+ if (!config || !config.enabled) return null;
3795
+ const now = /* @__PURE__ */ new Date();
3796
+ let nextDate = new Date(now);
3797
+ switch (config.type) {
3798
+ case "daily":
3799
+ nextDate.setDate(now.getDate() + 1);
3800
+ break;
3801
+ case "weekly":
3802
+ const targetDay = parseInt(config.day_of_week, 10);
3803
+ const currentDay = now.getDay();
3804
+ let daysUntilNext = targetDay - currentDay;
3805
+ if (daysUntilNext <= 0) daysUntilNext += 7;
3806
+ nextDate.setDate(now.getDate() + daysUntilNext);
3807
+ break;
3808
+ case "custom":
3809
+ if (config.custom_date) {
3810
+ const parts = config.custom_date.split("/");
3811
+ if (parts.length === 3) {
3812
+ nextDate = new Date(parseInt(parts[2], 10), parseInt(parts[1], 10) - 1, parseInt(parts[0], 10));
3813
+ } else {
3814
+ return null;
3815
+ }
3816
+ } else {
3817
+ return null;
3818
+ }
3819
+ break;
3820
+ default:
3821
+ return null;
3822
+ }
3823
+ const d = String(nextDate.getDate()).padStart(2, "0");
3824
+ const m = String(nextDate.getMonth() + 1).padStart(2, "0");
3825
+ const y = nextDate.getFullYear();
3826
+ return { date: `${d}/${m}/${y}` };
3827
+ }
3743
3828
 
3744
3829
  // src/components/CustomPropertyDisplay.jsx
3745
3830
  var import_react4 = __toESM(require("react"));
@@ -8866,7 +8951,15 @@ function InSceneQuestForm({
8866
8951
  const [size, setSize] = (0, import_react17.useState)("medium");
8867
8952
  const [intensity, setIntensity] = (0, import_react17.useState)(0);
8868
8953
  const [description, setDescription] = (0, import_react17.useState)("");
8954
+ const [recurrenceConfig, setRecurrenceConfig] = (0, import_react17.useState)({
8955
+ enabled: false,
8956
+ type: "daily",
8957
+ day_of_week: 1,
8958
+ custom_date: "",
8959
+ next_assignee_id: ""
8960
+ });
8869
8961
  const [reviewStack, setReviewStack] = (0, import_react17.useState)([]);
8962
+ const [questDate, setQuestDate] = (0, import_react17.useState)(null);
8870
8963
  const [isStatusDropdownOpen, setIsStatusDropdownOpen] = (0, import_react17.useState)(false);
8871
8964
  const [isAssigneeDropdownOpen, setIsAssigneeDropdownOpen] = (0, import_react17.useState)(false);
8872
8965
  const [assigneeSearchQuery, setAssigneeSearchQuery] = (0, import_react17.useState)("");
@@ -8938,6 +9031,8 @@ function InSceneQuestForm({
8938
9031
  textureImageUrl: null,
8939
9032
  reviewStack,
8940
9033
  timeline: initialTimeline,
9034
+ Date: questDate,
9035
+ recurrence_config: recurrenceConfig,
8941
9036
  ...additionalData
8942
9037
  });
8943
9038
  };
@@ -9104,7 +9199,139 @@ function InSceneQuestForm({
9104
9199
  )), viewMembers.filter((member) => {
9105
9200
  const search = assigneeSearchQuery.toLowerCase();
9106
9201
  return (member.name || "").toLowerCase().includes(search) || (member.email || "").toLowerCase().includes(search);
9107
- }).length === 0 && assigneeSearchQuery && /* @__PURE__ */ import_react17.default.createElement("li", { className: "px-3 py-4 text-xs text-slate-500 text-center italic" }, "Nenhum membro encontrado")))))), /* @__PURE__ */ import_react17.default.createElement("div", { className: "pt-2" }, /* @__PURE__ */ import_react17.default.createElement(
9202
+ }).length === 0 && assigneeSearchQuery && /* @__PURE__ */ import_react17.default.createElement("li", { className: "px-3 py-4 text-xs text-slate-500 text-center italic" }, "Nenhum membro encontrado")))))), /* @__PURE__ */ import_react17.default.createElement("div", { className: "space-y-1.5 mt-2" }, /* @__PURE__ */ import_react17.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ import_react17.default.createElement("label", { className: "text-xs text-slate-300 font-medium uppercase tracking-wider" }, "Prazo / Data"), !questDate && /* @__PURE__ */ import_react17.default.createElement(
9203
+ "button",
9204
+ {
9205
+ type: "button",
9206
+ onClick: () => setQuestDate({ type: "Data", value: "" }),
9207
+ className: "text-[10px] text-indigo-400 hover:text-indigo-300 font-bold uppercase transition-colors"
9208
+ },
9209
+ "+ Adicionar"
9210
+ )), questDate && /* @__PURE__ */ import_react17.default.createElement("div", { className: "p-3 bg-slate-900/60 rounded-xl border border-white/10 space-y-3 relative group" }, /* @__PURE__ */ import_react17.default.createElement(
9211
+ "button",
9212
+ {
9213
+ type: "button",
9214
+ onClick: () => setQuestDate(null),
9215
+ className: "absolute top-2 right-2 w-6 h-6 grid place-items-center rounded-full bg-red-500/10 text-red-400 opacity-0 group-hover:opacity-100 transition-opacity",
9216
+ title: "Remover data"
9217
+ },
9218
+ /* @__PURE__ */ import_react17.default.createElement(import_fi15.FiX, { size: 12 })
9219
+ ), /* @__PURE__ */ import_react17.default.createElement("div", { className: "flex items-center gap-1 rounded-lg bg-slate-800/50 p-1" }, ["Data", "Ano", "Date Interval"].map((t) => /* @__PURE__ */ import_react17.default.createElement(
9220
+ "button",
9221
+ {
9222
+ key: t,
9223
+ type: "button",
9224
+ onClick: () => {
9225
+ const newDate = { type: t };
9226
+ if (t === "Date Interval") {
9227
+ newDate.start = "";
9228
+ newDate.end = "";
9229
+ } else {
9230
+ newDate.value = "";
9231
+ }
9232
+ setQuestDate(newDate);
9233
+ },
9234
+ className: `flex-1 text-[10px] px-2 py-1.5 rounded-md transition-colors ${questDate.type === t ? "bg-indigo-600 text-white font-semibold shadow" : "text-slate-400 hover:bg-slate-700/50"}`
9235
+ },
9236
+ t === "Date Interval" ? "Intervalo" : t
9237
+ ))), questDate.type === "Data" && /* @__PURE__ */ import_react17.default.createElement(
9238
+ "input",
9239
+ {
9240
+ type: "text",
9241
+ placeholder: "DD/MM/AAAA",
9242
+ value: questDate.value || "",
9243
+ onChange: (e) => {
9244
+ const rawValue = e.target.value;
9245
+ const digits = rawValue.replace(/\D/g, "");
9246
+ const len = digits.length;
9247
+ let formattedValue = digits;
9248
+ if (len > 2 && len <= 4)
9249
+ formattedValue = `${digits.slice(0, 2)}/${digits.slice(2)}`;
9250
+ else if (len > 4)
9251
+ formattedValue = `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4, 8)}`;
9252
+ setQuestDate({ ...questDate, value: formattedValue });
9253
+ },
9254
+ maxLength: "10",
9255
+ className: "w-full bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-1 focus:ring-indigo-400/60"
9256
+ }
9257
+ ), questDate.type === "Ano" && /* @__PURE__ */ import_react17.default.createElement(
9258
+ "input",
9259
+ {
9260
+ type: "number",
9261
+ placeholder: "AAAA",
9262
+ value: questDate.value || "",
9263
+ onChange: (e) => setQuestDate({ ...questDate, value: e.target.value }),
9264
+ className: "w-full bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-1 focus:ring-indigo-400/60 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
9265
+ }
9266
+ ), questDate.type === "Date Interval" && /* @__PURE__ */ import_react17.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ import_react17.default.createElement(
9267
+ "input",
9268
+ {
9269
+ type: "number",
9270
+ placeholder: "In\xEDcio (Ano)",
9271
+ value: questDate.start || "",
9272
+ onChange: (e) => setQuestDate({ ...questDate, start: e.target.value }),
9273
+ className: "flex-1 bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-1 focus:ring-indigo-400/60 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
9274
+ }
9275
+ ), /* @__PURE__ */ import_react17.default.createElement("span", { className: "text-slate-500" }, "-"), /* @__PURE__ */ import_react17.default.createElement(
9276
+ "input",
9277
+ {
9278
+ type: "number",
9279
+ placeholder: "Fim (Ano)",
9280
+ value: questDate.end || "",
9281
+ onChange: (e) => setQuestDate({ ...questDate, end: e.target.value }),
9282
+ className: "flex-1 bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-1 focus:ring-indigo-400/60 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
9283
+ }
9284
+ )))), /* @__PURE__ */ import_react17.default.createElement("div", { className: "pt-4 border-t border-white/5 mt-2" }, /* @__PURE__ */ import_react17.default.createElement("div", { className: "flex items-center justify-between mb-3" }, /* @__PURE__ */ import_react17.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ import_react17.default.createElement(import_fi15.FiRepeat, { className: `w-3.5 h-3.5 ${recurrenceConfig.enabled ? "text-indigo-400" : "text-slate-500"}` }), /* @__PURE__ */ import_react17.default.createElement("span", { className: "text-xs text-slate-300 font-medium uppercase tracking-wider" }, "Recorr\xEAncia")), /* @__PURE__ */ import_react17.default.createElement(
9285
+ "button",
9286
+ {
9287
+ type: "button",
9288
+ onClick: () => setRecurrenceConfig((prev) => ({ ...prev, enabled: !prev.enabled })),
9289
+ className: `relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${recurrenceConfig.enabled ? "bg-indigo-600" : "bg-slate-700"} cursor-pointer`
9290
+ },
9291
+ /* @__PURE__ */ import_react17.default.createElement("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform ${recurrenceConfig.enabled ? "translate-x-4.5" : "translate-x-1"}` })
9292
+ )), recurrenceConfig.enabled && /* @__PURE__ */ import_react17.default.createElement("div", { className: "space-y-4 p-3 rounded-xl bg-white/5 border border-white/5 animate-in fade-in slide-in-from-top-1" }, /* @__PURE__ */ import_react17.default.createElement("div", { className: "grid grid-cols-2 gap-3" }, /* @__PURE__ */ import_react17.default.createElement("div", { className: "space-y-1.5" }, /* @__PURE__ */ import_react17.default.createElement("label", { className: "text-[10px] text-slate-500 uppercase font-bold tracking-tight" }, "Tipo"), /* @__PURE__ */ import_react17.default.createElement(
9293
+ "select",
9294
+ {
9295
+ value: recurrenceConfig.type,
9296
+ onChange: (e) => setRecurrenceConfig((prev) => ({ ...prev, type: e.target.value })),
9297
+ className: "w-full bg-slate-900/50 border border-white/10 rounded-lg px-2 py-1.5 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500/50"
9298
+ },
9299
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: "daily" }, "Di\xE1ria"),
9300
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: "weekly" }, "Semanal"),
9301
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: "custom" }, "Data Espec\xEDfica")
9302
+ )), recurrenceConfig.type === "weekly" && /* @__PURE__ */ import_react17.default.createElement("div", { className: "space-y-1.5" }, /* @__PURE__ */ import_react17.default.createElement("label", { className: "text-[10px] text-slate-500 uppercase font-bold tracking-tight" }, "Dia da Semana"), /* @__PURE__ */ import_react17.default.createElement(
9303
+ "select",
9304
+ {
9305
+ value: recurrenceConfig.day_of_week,
9306
+ onChange: (e) => setRecurrenceConfig((prev) => ({ ...prev, day_of_week: parseInt(e.target.value, 10) })),
9307
+ className: "w-full bg-slate-900/50 border border-white/10 rounded-lg px-2 py-1.5 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500/50"
9308
+ },
9309
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: 1 }, "Segunda-feira"),
9310
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: 2 }, "Ter\xE7a-feira"),
9311
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: 3 }, "Quarta-feira"),
9312
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: 4 }, "Quinta-feira"),
9313
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: 5 }, "Sexta-feira"),
9314
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: 6 }, "S\xE1bado"),
9315
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: 0 }, "Domingo")
9316
+ )), recurrenceConfig.type === "custom" && /* @__PURE__ */ import_react17.default.createElement("div", { className: "space-y-1.5" }, /* @__PURE__ */ import_react17.default.createElement("label", { className: "text-[10px] text-slate-500 uppercase font-bold tracking-tight" }, "Pr\xF3xima Data"), /* @__PURE__ */ import_react17.default.createElement(
9317
+ "input",
9318
+ {
9319
+ type: "text",
9320
+ placeholder: "DD/MM/AAAA",
9321
+ value: recurrenceConfig.custom_date,
9322
+ onChange: (e) => setRecurrenceConfig((prev) => ({ ...prev, custom_date: e.target.value })),
9323
+ className: "w-full bg-slate-900/50 border border-white/10 rounded-lg px-2 py-1.5 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500/50"
9324
+ }
9325
+ ))), /* @__PURE__ */ import_react17.default.createElement("div", { className: "space-y-1.5" }, /* @__PURE__ */ import_react17.default.createElement("label", { className: "text-[10px] text-slate-500 uppercase font-bold tracking-tight" }, "Pr\xF3ximo Respons\xE1vel"), /* @__PURE__ */ import_react17.default.createElement("div", { className: "relative" }, /* @__PURE__ */ import_react17.default.createElement(
9326
+ "select",
9327
+ {
9328
+ value: recurrenceConfig.next_assignee_id,
9329
+ onChange: (e) => setRecurrenceConfig((prev) => ({ ...prev, next_assignee_id: e.target.value })),
9330
+ className: "w-full bg-slate-900/50 border border-white/10 rounded-lg pl-8 pr-2 py-1.5 text-xs text-slate-200 appearance-none focus:outline-none focus:ring-1 focus:ring-indigo-500/50"
9331
+ },
9332
+ /* @__PURE__ */ import_react17.default.createElement("option", { value: "" }, "Sem respons\xE1vel"),
9333
+ viewMembers.map((member) => /* @__PURE__ */ import_react17.default.createElement("option", { key: member.id, value: member.id }, member.name || member.email))
9334
+ ), /* @__PURE__ */ import_react17.default.createElement(import_fi15.FiUser, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-500" }), /* @__PURE__ */ import_react17.default.createElement(import_fi15.FiChevronDown, { className: "absolute right-2.5 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-500 pointer-events-none" }))))), /* @__PURE__ */ import_react17.default.createElement("div", { className: "pt-2" }, /* @__PURE__ */ import_react17.default.createElement(
9108
9335
  QuestReviewStackEditor,
9109
9336
  {
9110
9337
  reviewStack,
@@ -9889,12 +10116,20 @@ function QuestDetailsPanel({
9889
10116
  const [isSaving, setIsSaving] = (0, import_react20.useState)(false);
9890
10117
  const [isLinkCopied, setIsLinkCopied] = (0, import_react20.useState)(false);
9891
10118
  const [hasUnsavedChanges, setHasUnsavedChanges] = (0, import_react20.useState)(false);
10119
+ const [questDate, setQuestDate] = (0, import_react20.useState)((node == null ? void 0 : node.Date) || null);
9892
10120
  const [noteText, setNoteText] = (0, import_react20.useState)("");
9893
10121
  const [noteLinks, setNoteLinks] = (0, import_react20.useState)([]);
9894
10122
  const [noteImages, setNoteImages] = (0, import_react20.useState)([]);
9895
10123
  const [isNoteFormOpen, setIsNoteFormOpen] = (0, import_react20.useState)(false);
9896
10124
  const [editingEntryId, setEditingEntryId] = (0, import_react20.useState)(null);
9897
10125
  const [editingEntryLabel, setEditingEntryLabel] = (0, import_react20.useState)("");
10126
+ const [recurrenceConfig, setRecurrenceConfig] = (0, import_react20.useState)((node == null ? void 0 : node.recurrence_config) || {
10127
+ enabled: false,
10128
+ type: "daily",
10129
+ day_of_week: 1,
10130
+ custom_date: "",
10131
+ next_assignee_id: ""
10132
+ });
9898
10133
  const maxPanelW = typeof window !== "undefined" ? window.innerWidth * 0.92 : 1200;
9899
10134
  const { width: panelWidth, isResizing, handlePointerDown: handleResize, setWidth } = useResizablePanel({
9900
10135
  initialWidth: isReadMode ? 700 : 440,
@@ -9930,6 +10165,14 @@ function QuestDetailsPanel({
9930
10165
  setCustomProps(extractCustomPropsFromNode(node || {}));
9931
10166
  setReviewStack((node == null ? void 0 : node.reviewStack) || []);
9932
10167
  setTimeline((node == null ? void 0 : node.timeline) || []);
10168
+ setQuestDate((node == null ? void 0 : node.Date) || null);
10169
+ setRecurrenceConfig((node == null ? void 0 : node.recurrence_config) || {
10170
+ enabled: false,
10171
+ type: "daily",
10172
+ day_of_week: 1,
10173
+ custom_date: "",
10174
+ next_assignee_id: ""
10175
+ });
9933
10176
  setHasUnsavedChanges(false);
9934
10177
  }
9935
10178
  }, [node]);
@@ -10025,6 +10268,7 @@ function QuestDetailsPanel({
10025
10268
  setIsSaving(false);
10026
10269
  }
10027
10270
  };
10271
+ const filteredCustomProps = customProps.filter((p) => p.key !== "Date" && p.key !== "Data");
10028
10272
  const handleAddTimelineNote = async () => {
10029
10273
  if (!noteText.trim() && noteLinks.length === 0 && noteImages.length === 0 || isSaving) return;
10030
10274
  setIsSaving(true);
@@ -10181,6 +10425,7 @@ ${noteText}` : noteText : entry.notes,
10181
10425
  const currentTimeline = overrides.timeline !== void 0 ? overrides.timeline : timeline;
10182
10426
  const currentExistingSections = overrides.existingSections !== void 0 ? overrides.existingSections : existingSections;
10183
10427
  const currentStatus = overrides.status !== void 0 ? overrides.status : status;
10428
+ const currentRecurrenceConfig = overrides.recurrenceConfig !== void 0 ? overrides.recurrenceConfig : recurrenceConfig;
10184
10429
  if (!keepOpen && !hasUnsavedChanges) {
10185
10430
  onClose();
10186
10431
  return;
@@ -10204,11 +10449,13 @@ ${noteText}` : noteText : entry.notes,
10204
10449
  status: currentStatus,
10205
10450
  assignee_id: currentAssigneeId || null,
10206
10451
  size,
10207
- description: currentDescription,
10452
+ description: currentDescription.trim(),
10208
10453
  description_sections: processedSections,
10454
+ intensity,
10455
+ recurrence_config: currentRecurrenceConfig,
10456
+ Date: questDate,
10209
10457
  useImageAsTexture: false,
10210
10458
  textureImageUrl: null,
10211
- intensity,
10212
10459
  is_quest: true,
10213
10460
  reviewStack: currentReviewStack,
10214
10461
  timeline: currentTimeline,
@@ -12570,7 +12817,7 @@ function XViewScene({
12570
12817
  }
12571
12818
  }
12572
12819
  Object.values(nodeObjects).forEach((mesh) => {
12573
- const oldLabel = mesh.getObjectByName("timelineLabel");
12820
+ const oldLabel = mesh.getObjectByName("timelineLabel") || mesh.getObjectByName("questDateLabel");
12574
12821
  if (oldLabel) {
12575
12822
  mesh.remove(oldLabel);
12576
12823
  if (oldLabel.material.map) oldLabel.material.map.dispose();
@@ -13003,8 +13250,16 @@ function XViewScene({
13003
13250
  );
13004
13251
  const handleActivateQuestLens = (0, import_react28.useCallback)(({ assigneeId, status: status2 }) => {
13005
13252
  var _a2;
13006
- const { nodeObjects, tweenGroup, graphGroup } = stateRef.current;
13253
+ const { nodeObjects, tweenGroup, graphGroup, timelineIntervalsGroup } = stateRef.current;
13007
13254
  if (!nodeObjects || !tweenGroup || !graphGroup) return;
13255
+ Object.values(nodeObjects).forEach((mesh) => {
13256
+ const oldLabel = mesh.getObjectByName("questDateLabel") || mesh.getObjectByName("timelineLabel");
13257
+ if (oldLabel) {
13258
+ mesh.remove(oldLabel);
13259
+ if (oldLabel.material.map) oldLabel.material.map.dispose();
13260
+ oldLabel.material.dispose();
13261
+ }
13262
+ });
13008
13263
  const allQuests = ((_a2 = sceneDataRef.current) == null ? void 0 : _a2.quest_nodes) || [];
13009
13264
  const filteredQuests = allQuests.filter((q) => {
13010
13265
  const matchAssignee = !assigneeId || String(q.assignee_id) === String(assigneeId);
@@ -13026,6 +13281,60 @@ function XViewScene({
13026
13281
  questsByStatus[q.status].push(q);
13027
13282
  }
13028
13283
  });
13284
+ const parseDate = (str) => {
13285
+ if (!str) return null;
13286
+ const parts = str.split("/");
13287
+ if (parts.length === 3) {
13288
+ const day = parseInt(parts[0], 10);
13289
+ const month = parseInt(parts[1], 10) - 1;
13290
+ const year = parseInt(parts[2], 10);
13291
+ if (!isNaN(day) && !isNaN(month) && !isNaN(year)) {
13292
+ return new Date(year, month, day);
13293
+ }
13294
+ }
13295
+ return null;
13296
+ };
13297
+ const timePoints = /* @__PURE__ */ new Set();
13298
+ const questTimeInfos = /* @__PURE__ */ new Map();
13299
+ filteredQuests.forEach((q) => {
13300
+ const dateProp = q.Date;
13301
+ let info = { isUndated: true, label: "" };
13302
+ if (dateProp) {
13303
+ if (dateProp.date) {
13304
+ const d = parseDate(dateProp.date);
13305
+ if (d) {
13306
+ info = { startDate: d, endDate: d, label: dateProp.date, isUndated: false };
13307
+ timePoints.add(d.getTime());
13308
+ }
13309
+ } else if (dateProp.year) {
13310
+ const y = parseInt(dateProp.year, 10);
13311
+ if (!isNaN(y)) {
13312
+ const d = new Date(y, 0, 1);
13313
+ info = { startDate: d, endDate: d, label: String(y), isUndated: false };
13314
+ timePoints.add(d.getTime());
13315
+ }
13316
+ } else if (dateProp.date_interval && dateProp.date_interval.start) {
13317
+ const sY = parseInt(dateProp.date_interval.start, 10);
13318
+ const eY = parseInt(dateProp.date_interval.end, 10) || sY;
13319
+ if (!isNaN(sY)) {
13320
+ const sd = new Date(sY, 0, 1);
13321
+ const ed = new Date(eY, 11, 31);
13322
+ info = { startDate: sd, endDate: ed, label: `${sY}-${eY}`, isUndated: false };
13323
+ timePoints.add(sd.getTime());
13324
+ timePoints.add(ed.getTime());
13325
+ }
13326
+ }
13327
+ }
13328
+ questTimeInfos.set(String(q.id), info);
13329
+ });
13330
+ const sortedTimePoints = Array.from(timePoints).sort((a, b) => a - b);
13331
+ const timeToYMap = /* @__PURE__ */ new Map();
13332
+ if (sortedTimePoints.length > 0) {
13333
+ const maxIdx = sortedTimePoints.length - 1;
13334
+ sortedTimePoints.forEach((t, i) => {
13335
+ timeToYMap.set(t, (i - maxIdx) * (QUEST_LENS_ROW_SPACING || 30));
13336
+ });
13337
+ }
13029
13338
  const startX = -((QUEST_STATUS_ORDER.length - 1) * QUEST_LENS_COLUMN_SPACING) / 2;
13030
13339
  Object.keys(nodeObjects).forEach((id) => {
13031
13340
  const mesh = nodeObjects[id];
@@ -13062,10 +13371,23 @@ function XViewScene({
13062
13371
  if (!mesh) {
13063
13372
  mesh = addOrUpdateNodeMesh(q, new THREE3.Vector3(colX, 150, 0), true);
13064
13373
  }
13065
- const targetY = (questsInCol.length - 1) * 0.5 * QUEST_LENS_ROW_SPACING - rowIndex * QUEST_LENS_ROW_SPACING;
13374
+ const timeInfo = questTimeInfos.get(nodeId) || { isUndated: true };
13375
+ let targetY;
13376
+ if (!timeInfo.isUndated) {
13377
+ targetY = timeToYMap.get(timeInfo.startDate.getTime());
13378
+ } else {
13379
+ const undatedOffset = sortedTimePoints.length > 0 ? sortedTimePoints.length * 5 : 0;
13380
+ targetY = -80 - rowIndex * (QUEST_LENS_ROW_SPACING || 30) - undatedOffset;
13381
+ }
13066
13382
  const lensTween = new import_tween2.Tween(mesh.position).to({ x: colX, y: targetY, z: 0 }, 1500).easing(import_tween2.Easing.Cubic.InOut);
13067
13383
  tweenGroup.add(lensTween);
13068
13384
  lensTween.start();
13385
+ if (!timeInfo.isUndated && timeInfo.label) {
13386
+ const dateSprite = createTextSprite(timeInfo.label, 36);
13387
+ dateSprite.name = "questDateLabel";
13388
+ dateSprite.position.set(0, 3.5, 0);
13389
+ mesh.add(dateSprite);
13390
+ }
13069
13391
  if (mesh.userData.labelObject) {
13070
13392
  const labelTargetPos = new THREE3.Vector3(colX, targetY, 0).add(
13071
13393
  mesh.userData.labelOffset || new THREE3.Vector3(0, -2.5, 0)
package/dist/index.mjs CHANGED
@@ -2812,7 +2812,7 @@ var userActionHandlers = {
2812
2812
  }
2813
2813
  },
2814
2814
  handleSaveNodeDetails: async (context, updatedNode, keepOpen = false) => {
2815
- var _a;
2815
+ var _a, _b, _c;
2816
2816
  const { graphDataRef, sceneDataRef, stateRef, setters } = context;
2817
2817
  if (!graphDataRef.current || !sceneDataRef.current) return;
2818
2818
  const {
@@ -2833,15 +2833,66 @@ var userActionHandlers = {
2833
2833
  const { parentFileId, ownerId } = parentInfo;
2834
2834
  const specificParentData = JSON.parse(JSON.stringify(graphDataRef.current[parentFileId]));
2835
2835
  const nodeIndex = specificParentData.nodes.findIndex((n) => String(n.id) === String(nodeToSave.id));
2836
- if (nodeIndex > -1) {
2837
- specificParentData.nodes[nodeIndex] = { ...nodeToSave };
2838
- } else {
2836
+ if (nodeIndex === -1) {
2839
2837
  console.error("Node n\xE3o encontrado nos dados do parent para atualiza\xE7\xE3o:", nodeToSave.id);
2840
2838
  alert("Erro interno: Node n\xE3o encontrado para salvar.");
2841
2839
  return;
2842
2840
  }
2841
+ const oldNode = specificParentData.nodes[nodeIndex];
2842
+ const isNewDone = oldNode.status !== "Done" && nodeToSave.status === "Done";
2843
+ specificParentData.nodes[nodeIndex] = { ...nodeToSave };
2844
+ if (isNewDone && nodeToSave.is_quest && ((_a = nodeToSave.recurrence_config) == null ? void 0 : _a.enabled)) {
2845
+ const nextDateObj = calculateNextRecurrenceDate(nodeToSave.recurrence_config);
2846
+ if (nextDateObj) {
2847
+ const currentCounter = sceneDataRef.current.quest_counter || 1;
2848
+ const nextCounter = currentCounter + 1;
2849
+ sceneDataRef.current.quest_counter = nextCounter;
2850
+ let nextName = nodeToSave.raw_title || nodeToSave.name;
2851
+ const prefixParts = (nodeToSave.name || "").split(" - \xBB ");
2852
+ if (prefixParts.length > 1) {
2853
+ const prefixBase = prefixParts[0].split(" - ")[0];
2854
+ nextName = `${prefixBase} - ${nextCounter} - \xBB ${nodeToSave.raw_title || prefixParts[1]}`;
2855
+ }
2856
+ const nextRecurrenceConfig = { ...nodeToSave.recurrence_config };
2857
+ if (nextRecurrenceConfig.type === "custom") {
2858
+ nextRecurrenceConfig.custom_date = "";
2859
+ }
2860
+ const nextQuestNode = {
2861
+ ...JSON.parse(JSON.stringify(nodeToSave)),
2862
+ // Deep copy properties
2863
+ id: short.generate(),
2864
+ name: nextName,
2865
+ status: "Backlog",
2866
+ color: "#64748b",
2867
+ // QUEST_STATUS_COLORS["Backlog"]
2868
+ assignee_id: nodeToSave.recurrence_config.next_assignee_id || "",
2869
+ Date: nextDateObj,
2870
+ timeline: [{
2871
+ id: short.generate(),
2872
+ type: "status_changed",
2873
+ timestamp: Date.now(),
2874
+ actorId: "system",
2875
+ description: `Quest criada automaticamente via recorr\xEAncia a partir da quest #${nodeToSave.id}`,
2876
+ links: [],
2877
+ images: [],
2878
+ metadata: { status: "Backlog" }
2879
+ }],
2880
+ version_node: {
2881
+ is_version: true,
2882
+ parent_node: ((_b = nodeToSave.version_node) == null ? void 0 : _b.parent_node) || nodeToSave.id
2883
+ },
2884
+ recurrence_config: nextRecurrenceConfig
2885
+ };
2886
+ specificParentData.nodes.push(nextQuestNode);
2887
+ if (stateRef.current.nodeObjects[String(nodeToSave.id)]) {
2888
+ const pos = stateRef.current.nodeObjects[String(nodeToSave.id)].position.clone();
2889
+ pos.x += 12;
2890
+ addStandaloneNodeToScene(stateRef.current, nextQuestNode, pos);
2891
+ }
2892
+ }
2893
+ }
2843
2894
  try {
2844
- const isView = ((_a = context.viewType) == null ? void 0 : _a.toLowerCase()) === "view";
2895
+ const isView = ((_c = context.viewType) == null ? void 0 : _c.toLowerCase()) === "view";
2845
2896
  if (isView && parentFileId === context.sceneConfigId) {
2846
2897
  const visualNodeIndex = sceneDataRef.current.nodes.findIndex((n) => String(n.id) === String(nodeToSave.id));
2847
2898
  if (visualNodeIndex > -1) {
@@ -3697,10 +3748,44 @@ function createTimelineEntry(actorId, description, type = "comment", extras = {}
3697
3748
  id: img.id || v4_default(),
3698
3749
  name: img.name || "",
3699
3750
  value: img.value || ""
3700
- })),
3701
- metadata
3751
+ }))
3702
3752
  };
3703
3753
  }
3754
+ function calculateNextRecurrenceDate(config) {
3755
+ if (!config || !config.enabled) return null;
3756
+ const now = /* @__PURE__ */ new Date();
3757
+ let nextDate = new Date(now);
3758
+ switch (config.type) {
3759
+ case "daily":
3760
+ nextDate.setDate(now.getDate() + 1);
3761
+ break;
3762
+ case "weekly":
3763
+ const targetDay = parseInt(config.day_of_week, 10);
3764
+ const currentDay = now.getDay();
3765
+ let daysUntilNext = targetDay - currentDay;
3766
+ if (daysUntilNext <= 0) daysUntilNext += 7;
3767
+ nextDate.setDate(now.getDate() + daysUntilNext);
3768
+ break;
3769
+ case "custom":
3770
+ if (config.custom_date) {
3771
+ const parts = config.custom_date.split("/");
3772
+ if (parts.length === 3) {
3773
+ nextDate = new Date(parseInt(parts[2], 10), parseInt(parts[1], 10) - 1, parseInt(parts[0], 10));
3774
+ } else {
3775
+ return null;
3776
+ }
3777
+ } else {
3778
+ return null;
3779
+ }
3780
+ break;
3781
+ default:
3782
+ return null;
3783
+ }
3784
+ const d = String(nextDate.getDate()).padStart(2, "0");
3785
+ const m = String(nextDate.getMonth() + 1).padStart(2, "0");
3786
+ const y = nextDate.getFullYear();
3787
+ return { date: `${d}/${m}/${y}` };
3788
+ }
3704
3789
 
3705
3790
  // src/components/CustomPropertyDisplay.jsx
3706
3791
  import React3, { useState as useState4, useRef as useRef3, useEffect as useEffect3 } from "react";
@@ -8684,7 +8769,9 @@ import {
8684
8769
  FiX as FiX5,
8685
8770
  FiChevronDown as FiChevronDown6,
8686
8771
  FiUser as FiUser2,
8687
- FiSearch as FiSearch5
8772
+ FiSearch as FiSearch5,
8773
+ FiCalendar,
8774
+ FiRepeat
8688
8775
  } from "react-icons/fi";
8689
8776
 
8690
8777
  // src/components/QuestReviewStackEditor.jsx
@@ -8883,7 +8970,15 @@ function InSceneQuestForm({
8883
8970
  const [size, setSize] = useState17("medium");
8884
8971
  const [intensity, setIntensity] = useState17(0);
8885
8972
  const [description, setDescription] = useState17("");
8973
+ const [recurrenceConfig, setRecurrenceConfig] = useState17({
8974
+ enabled: false,
8975
+ type: "daily",
8976
+ day_of_week: 1,
8977
+ custom_date: "",
8978
+ next_assignee_id: ""
8979
+ });
8886
8980
  const [reviewStack, setReviewStack] = useState17([]);
8981
+ const [questDate, setQuestDate] = useState17(null);
8887
8982
  const [isStatusDropdownOpen, setIsStatusDropdownOpen] = useState17(false);
8888
8983
  const [isAssigneeDropdownOpen, setIsAssigneeDropdownOpen] = useState17(false);
8889
8984
  const [assigneeSearchQuery, setAssigneeSearchQuery] = useState17("");
@@ -8955,6 +9050,8 @@ function InSceneQuestForm({
8955
9050
  textureImageUrl: null,
8956
9051
  reviewStack,
8957
9052
  timeline: initialTimeline,
9053
+ Date: questDate,
9054
+ recurrence_config: recurrenceConfig,
8958
9055
  ...additionalData
8959
9056
  });
8960
9057
  };
@@ -9121,7 +9218,139 @@ function InSceneQuestForm({
9121
9218
  )), viewMembers.filter((member) => {
9122
9219
  const search = assigneeSearchQuery.toLowerCase();
9123
9220
  return (member.name || "").toLowerCase().includes(search) || (member.email || "").toLowerCase().includes(search);
9124
- }).length === 0 && assigneeSearchQuery && /* @__PURE__ */ React16.createElement("li", { className: "px-3 py-4 text-xs text-slate-500 text-center italic" }, "Nenhum membro encontrado")))))), /* @__PURE__ */ React16.createElement("div", { className: "pt-2" }, /* @__PURE__ */ React16.createElement(
9221
+ }).length === 0 && assigneeSearchQuery && /* @__PURE__ */ React16.createElement("li", { className: "px-3 py-4 text-xs text-slate-500 text-center italic" }, "Nenhum membro encontrado")))))), /* @__PURE__ */ React16.createElement("div", { className: "space-y-1.5 mt-2" }, /* @__PURE__ */ React16.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React16.createElement("label", { className: "text-xs text-slate-300 font-medium uppercase tracking-wider" }, "Prazo / Data"), !questDate && /* @__PURE__ */ React16.createElement(
9222
+ "button",
9223
+ {
9224
+ type: "button",
9225
+ onClick: () => setQuestDate({ type: "Data", value: "" }),
9226
+ className: "text-[10px] text-indigo-400 hover:text-indigo-300 font-bold uppercase transition-colors"
9227
+ },
9228
+ "+ Adicionar"
9229
+ )), questDate && /* @__PURE__ */ React16.createElement("div", { className: "p-3 bg-slate-900/60 rounded-xl border border-white/10 space-y-3 relative group" }, /* @__PURE__ */ React16.createElement(
9230
+ "button",
9231
+ {
9232
+ type: "button",
9233
+ onClick: () => setQuestDate(null),
9234
+ className: "absolute top-2 right-2 w-6 h-6 grid place-items-center rounded-full bg-red-500/10 text-red-400 opacity-0 group-hover:opacity-100 transition-opacity",
9235
+ title: "Remover data"
9236
+ },
9237
+ /* @__PURE__ */ React16.createElement(FiX5, { size: 12 })
9238
+ ), /* @__PURE__ */ React16.createElement("div", { className: "flex items-center gap-1 rounded-lg bg-slate-800/50 p-1" }, ["Data", "Ano", "Date Interval"].map((t) => /* @__PURE__ */ React16.createElement(
9239
+ "button",
9240
+ {
9241
+ key: t,
9242
+ type: "button",
9243
+ onClick: () => {
9244
+ const newDate = { type: t };
9245
+ if (t === "Date Interval") {
9246
+ newDate.start = "";
9247
+ newDate.end = "";
9248
+ } else {
9249
+ newDate.value = "";
9250
+ }
9251
+ setQuestDate(newDate);
9252
+ },
9253
+ className: `flex-1 text-[10px] px-2 py-1.5 rounded-md transition-colors ${questDate.type === t ? "bg-indigo-600 text-white font-semibold shadow" : "text-slate-400 hover:bg-slate-700/50"}`
9254
+ },
9255
+ t === "Date Interval" ? "Intervalo" : t
9256
+ ))), questDate.type === "Data" && /* @__PURE__ */ React16.createElement(
9257
+ "input",
9258
+ {
9259
+ type: "text",
9260
+ placeholder: "DD/MM/AAAA",
9261
+ value: questDate.value || "",
9262
+ onChange: (e) => {
9263
+ const rawValue = e.target.value;
9264
+ const digits = rawValue.replace(/\D/g, "");
9265
+ const len = digits.length;
9266
+ let formattedValue = digits;
9267
+ if (len > 2 && len <= 4)
9268
+ formattedValue = `${digits.slice(0, 2)}/${digits.slice(2)}`;
9269
+ else if (len > 4)
9270
+ formattedValue = `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4, 8)}`;
9271
+ setQuestDate({ ...questDate, value: formattedValue });
9272
+ },
9273
+ maxLength: "10",
9274
+ className: "w-full bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-1 focus:ring-indigo-400/60"
9275
+ }
9276
+ ), questDate.type === "Ano" && /* @__PURE__ */ React16.createElement(
9277
+ "input",
9278
+ {
9279
+ type: "number",
9280
+ placeholder: "AAAA",
9281
+ value: questDate.value || "",
9282
+ onChange: (e) => setQuestDate({ ...questDate, value: e.target.value }),
9283
+ className: "w-full bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-1 focus:ring-indigo-400/60 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
9284
+ }
9285
+ ), questDate.type === "Date Interval" && /* @__PURE__ */ React16.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React16.createElement(
9286
+ "input",
9287
+ {
9288
+ type: "number",
9289
+ placeholder: "In\xEDcio (Ano)",
9290
+ value: questDate.start || "",
9291
+ onChange: (e) => setQuestDate({ ...questDate, start: e.target.value }),
9292
+ className: "flex-1 bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-1 focus:ring-indigo-400/60 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
9293
+ }
9294
+ ), /* @__PURE__ */ React16.createElement("span", { className: "text-slate-500" }, "-"), /* @__PURE__ */ React16.createElement(
9295
+ "input",
9296
+ {
9297
+ type: "number",
9298
+ placeholder: "Fim (Ano)",
9299
+ value: questDate.end || "",
9300
+ onChange: (e) => setQuestDate({ ...questDate, end: e.target.value }),
9301
+ className: "flex-1 bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-1 focus:ring-indigo-400/60 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
9302
+ }
9303
+ )))), /* @__PURE__ */ React16.createElement("div", { className: "pt-4 border-t border-white/5 mt-2" }, /* @__PURE__ */ React16.createElement("div", { className: "flex items-center justify-between mb-3" }, /* @__PURE__ */ React16.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React16.createElement(FiRepeat, { className: `w-3.5 h-3.5 ${recurrenceConfig.enabled ? "text-indigo-400" : "text-slate-500"}` }), /* @__PURE__ */ React16.createElement("span", { className: "text-xs text-slate-300 font-medium uppercase tracking-wider" }, "Recorr\xEAncia")), /* @__PURE__ */ React16.createElement(
9304
+ "button",
9305
+ {
9306
+ type: "button",
9307
+ onClick: () => setRecurrenceConfig((prev) => ({ ...prev, enabled: !prev.enabled })),
9308
+ className: `relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${recurrenceConfig.enabled ? "bg-indigo-600" : "bg-slate-700"} cursor-pointer`
9309
+ },
9310
+ /* @__PURE__ */ React16.createElement("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform ${recurrenceConfig.enabled ? "translate-x-4.5" : "translate-x-1"}` })
9311
+ )), recurrenceConfig.enabled && /* @__PURE__ */ React16.createElement("div", { className: "space-y-4 p-3 rounded-xl bg-white/5 border border-white/5 animate-in fade-in slide-in-from-top-1" }, /* @__PURE__ */ React16.createElement("div", { className: "grid grid-cols-2 gap-3" }, /* @__PURE__ */ React16.createElement("div", { className: "space-y-1.5" }, /* @__PURE__ */ React16.createElement("label", { className: "text-[10px] text-slate-500 uppercase font-bold tracking-tight" }, "Tipo"), /* @__PURE__ */ React16.createElement(
9312
+ "select",
9313
+ {
9314
+ value: recurrenceConfig.type,
9315
+ onChange: (e) => setRecurrenceConfig((prev) => ({ ...prev, type: e.target.value })),
9316
+ className: "w-full bg-slate-900/50 border border-white/10 rounded-lg px-2 py-1.5 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500/50"
9317
+ },
9318
+ /* @__PURE__ */ React16.createElement("option", { value: "daily" }, "Di\xE1ria"),
9319
+ /* @__PURE__ */ React16.createElement("option", { value: "weekly" }, "Semanal"),
9320
+ /* @__PURE__ */ React16.createElement("option", { value: "custom" }, "Data Espec\xEDfica")
9321
+ )), recurrenceConfig.type === "weekly" && /* @__PURE__ */ React16.createElement("div", { className: "space-y-1.5" }, /* @__PURE__ */ React16.createElement("label", { className: "text-[10px] text-slate-500 uppercase font-bold tracking-tight" }, "Dia da Semana"), /* @__PURE__ */ React16.createElement(
9322
+ "select",
9323
+ {
9324
+ value: recurrenceConfig.day_of_week,
9325
+ onChange: (e) => setRecurrenceConfig((prev) => ({ ...prev, day_of_week: parseInt(e.target.value, 10) })),
9326
+ className: "w-full bg-slate-900/50 border border-white/10 rounded-lg px-2 py-1.5 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500/50"
9327
+ },
9328
+ /* @__PURE__ */ React16.createElement("option", { value: 1 }, "Segunda-feira"),
9329
+ /* @__PURE__ */ React16.createElement("option", { value: 2 }, "Ter\xE7a-feira"),
9330
+ /* @__PURE__ */ React16.createElement("option", { value: 3 }, "Quarta-feira"),
9331
+ /* @__PURE__ */ React16.createElement("option", { value: 4 }, "Quinta-feira"),
9332
+ /* @__PURE__ */ React16.createElement("option", { value: 5 }, "Sexta-feira"),
9333
+ /* @__PURE__ */ React16.createElement("option", { value: 6 }, "S\xE1bado"),
9334
+ /* @__PURE__ */ React16.createElement("option", { value: 0 }, "Domingo")
9335
+ )), recurrenceConfig.type === "custom" && /* @__PURE__ */ React16.createElement("div", { className: "space-y-1.5" }, /* @__PURE__ */ React16.createElement("label", { className: "text-[10px] text-slate-500 uppercase font-bold tracking-tight" }, "Pr\xF3xima Data"), /* @__PURE__ */ React16.createElement(
9336
+ "input",
9337
+ {
9338
+ type: "text",
9339
+ placeholder: "DD/MM/AAAA",
9340
+ value: recurrenceConfig.custom_date,
9341
+ onChange: (e) => setRecurrenceConfig((prev) => ({ ...prev, custom_date: e.target.value })),
9342
+ className: "w-full bg-slate-900/50 border border-white/10 rounded-lg px-2 py-1.5 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500/50"
9343
+ }
9344
+ ))), /* @__PURE__ */ React16.createElement("div", { className: "space-y-1.5" }, /* @__PURE__ */ React16.createElement("label", { className: "text-[10px] text-slate-500 uppercase font-bold tracking-tight" }, "Pr\xF3ximo Respons\xE1vel"), /* @__PURE__ */ React16.createElement("div", { className: "relative" }, /* @__PURE__ */ React16.createElement(
9345
+ "select",
9346
+ {
9347
+ value: recurrenceConfig.next_assignee_id,
9348
+ onChange: (e) => setRecurrenceConfig((prev) => ({ ...prev, next_assignee_id: e.target.value })),
9349
+ className: "w-full bg-slate-900/50 border border-white/10 rounded-lg pl-8 pr-2 py-1.5 text-xs text-slate-200 appearance-none focus:outline-none focus:ring-1 focus:ring-indigo-500/50"
9350
+ },
9351
+ /* @__PURE__ */ React16.createElement("option", { value: "" }, "Sem respons\xE1vel"),
9352
+ viewMembers.map((member) => /* @__PURE__ */ React16.createElement("option", { key: member.id, value: member.id }, member.name || member.email))
9353
+ ), /* @__PURE__ */ React16.createElement(FiUser2, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-500" }), /* @__PURE__ */ React16.createElement(FiChevronDown6, { className: "absolute right-2.5 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-500 pointer-events-none" }))))), /* @__PURE__ */ React16.createElement("div", { className: "pt-2" }, /* @__PURE__ */ React16.createElement(
9125
9354
  QuestReviewStackEditor,
9126
9355
  {
9127
9356
  reviewStack,
@@ -9776,7 +10005,7 @@ function NodeDetailsPanel({
9776
10005
 
9777
10006
  // src/components/QuestDetailsPanel.jsx
9778
10007
  import React19, { useState as useState19, useEffect as useEffect16, useRef as useRef14 } from "react";
9779
- import { FiPlus as FiPlus8, FiX as FiX7, FiCheck as FiCheck11, FiEdit2 as FiEdit28, FiLoader as FiLoader3, FiBookOpen as FiBookOpen4, FiLink as FiLink7, FiTarget as FiTarget3, FiChevronDown as FiChevronDown7, FiUser as FiUser3, FiSearch as FiSearch7 } from "react-icons/fi";
10008
+ import { FiPlus as FiPlus8, FiX as FiX7, FiCheck as FiCheck11, FiEdit2 as FiEdit28, FiLoader as FiLoader3, FiBookOpen as FiBookOpen4, FiLink as FiLink7, FiTarget as FiTarget3, FiChevronDown as FiChevronDown7, FiUser as FiUser3, FiSearch as FiSearch7, FiRepeat as FiRepeat2 } from "react-icons/fi";
9780
10009
 
9781
10010
  // src/components/QuestTimeline.jsx
9782
10011
  import React18 from "react";
@@ -9918,12 +10147,20 @@ function QuestDetailsPanel({
9918
10147
  const [isSaving, setIsSaving] = useState19(false);
9919
10148
  const [isLinkCopied, setIsLinkCopied] = useState19(false);
9920
10149
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState19(false);
10150
+ const [questDate, setQuestDate] = useState19((node == null ? void 0 : node.Date) || null);
9921
10151
  const [noteText, setNoteText] = useState19("");
9922
10152
  const [noteLinks, setNoteLinks] = useState19([]);
9923
10153
  const [noteImages, setNoteImages] = useState19([]);
9924
10154
  const [isNoteFormOpen, setIsNoteFormOpen] = useState19(false);
9925
10155
  const [editingEntryId, setEditingEntryId] = useState19(null);
9926
10156
  const [editingEntryLabel, setEditingEntryLabel] = useState19("");
10157
+ const [recurrenceConfig, setRecurrenceConfig] = useState19((node == null ? void 0 : node.recurrence_config) || {
10158
+ enabled: false,
10159
+ type: "daily",
10160
+ day_of_week: 1,
10161
+ custom_date: "",
10162
+ next_assignee_id: ""
10163
+ });
9927
10164
  const maxPanelW = typeof window !== "undefined" ? window.innerWidth * 0.92 : 1200;
9928
10165
  const { width: panelWidth, isResizing, handlePointerDown: handleResize, setWidth } = useResizablePanel({
9929
10166
  initialWidth: isReadMode ? 700 : 440,
@@ -9959,6 +10196,14 @@ function QuestDetailsPanel({
9959
10196
  setCustomProps(extractCustomPropsFromNode(node || {}));
9960
10197
  setReviewStack((node == null ? void 0 : node.reviewStack) || []);
9961
10198
  setTimeline((node == null ? void 0 : node.timeline) || []);
10199
+ setQuestDate((node == null ? void 0 : node.Date) || null);
10200
+ setRecurrenceConfig((node == null ? void 0 : node.recurrence_config) || {
10201
+ enabled: false,
10202
+ type: "daily",
10203
+ day_of_week: 1,
10204
+ custom_date: "",
10205
+ next_assignee_id: ""
10206
+ });
9962
10207
  setHasUnsavedChanges(false);
9963
10208
  }
9964
10209
  }, [node]);
@@ -10054,6 +10299,7 @@ function QuestDetailsPanel({
10054
10299
  setIsSaving(false);
10055
10300
  }
10056
10301
  };
10302
+ const filteredCustomProps = customProps.filter((p) => p.key !== "Date" && p.key !== "Data");
10057
10303
  const handleAddTimelineNote = async () => {
10058
10304
  if (!noteText.trim() && noteLinks.length === 0 && noteImages.length === 0 || isSaving) return;
10059
10305
  setIsSaving(true);
@@ -10210,6 +10456,7 @@ ${noteText}` : noteText : entry.notes,
10210
10456
  const currentTimeline = overrides.timeline !== void 0 ? overrides.timeline : timeline;
10211
10457
  const currentExistingSections = overrides.existingSections !== void 0 ? overrides.existingSections : existingSections;
10212
10458
  const currentStatus = overrides.status !== void 0 ? overrides.status : status;
10459
+ const currentRecurrenceConfig = overrides.recurrenceConfig !== void 0 ? overrides.recurrenceConfig : recurrenceConfig;
10213
10460
  if (!keepOpen && !hasUnsavedChanges) {
10214
10461
  onClose();
10215
10462
  return;
@@ -10233,11 +10480,13 @@ ${noteText}` : noteText : entry.notes,
10233
10480
  status: currentStatus,
10234
10481
  assignee_id: currentAssigneeId || null,
10235
10482
  size,
10236
- description: currentDescription,
10483
+ description: currentDescription.trim(),
10237
10484
  description_sections: processedSections,
10485
+ intensity,
10486
+ recurrence_config: currentRecurrenceConfig,
10487
+ Date: questDate,
10238
10488
  useImageAsTexture: false,
10239
10489
  textureImageUrl: null,
10240
- intensity,
10241
10490
  is_quest: true,
10242
10491
  reviewStack: currentReviewStack,
10243
10492
  timeline: currentTimeline,
@@ -12612,7 +12861,7 @@ function XViewScene({
12612
12861
  }
12613
12862
  }
12614
12863
  Object.values(nodeObjects).forEach((mesh) => {
12615
- const oldLabel = mesh.getObjectByName("timelineLabel");
12864
+ const oldLabel = mesh.getObjectByName("timelineLabel") || mesh.getObjectByName("questDateLabel");
12616
12865
  if (oldLabel) {
12617
12866
  mesh.remove(oldLabel);
12618
12867
  if (oldLabel.material.map) oldLabel.material.map.dispose();
@@ -13045,8 +13294,16 @@ function XViewScene({
13045
13294
  );
13046
13295
  const handleActivateQuestLens = useCallback4(({ assigneeId, status: status2 }) => {
13047
13296
  var _a2;
13048
- const { nodeObjects, tweenGroup, graphGroup } = stateRef.current;
13297
+ const { nodeObjects, tweenGroup, graphGroup, timelineIntervalsGroup } = stateRef.current;
13049
13298
  if (!nodeObjects || !tweenGroup || !graphGroup) return;
13299
+ Object.values(nodeObjects).forEach((mesh) => {
13300
+ const oldLabel = mesh.getObjectByName("questDateLabel") || mesh.getObjectByName("timelineLabel");
13301
+ if (oldLabel) {
13302
+ mesh.remove(oldLabel);
13303
+ if (oldLabel.material.map) oldLabel.material.map.dispose();
13304
+ oldLabel.material.dispose();
13305
+ }
13306
+ });
13050
13307
  const allQuests = ((_a2 = sceneDataRef.current) == null ? void 0 : _a2.quest_nodes) || [];
13051
13308
  const filteredQuests = allQuests.filter((q) => {
13052
13309
  const matchAssignee = !assigneeId || String(q.assignee_id) === String(assigneeId);
@@ -13068,6 +13325,60 @@ function XViewScene({
13068
13325
  questsByStatus[q.status].push(q);
13069
13326
  }
13070
13327
  });
13328
+ const parseDate = (str) => {
13329
+ if (!str) return null;
13330
+ const parts = str.split("/");
13331
+ if (parts.length === 3) {
13332
+ const day = parseInt(parts[0], 10);
13333
+ const month = parseInt(parts[1], 10) - 1;
13334
+ const year = parseInt(parts[2], 10);
13335
+ if (!isNaN(day) && !isNaN(month) && !isNaN(year)) {
13336
+ return new Date(year, month, day);
13337
+ }
13338
+ }
13339
+ return null;
13340
+ };
13341
+ const timePoints = /* @__PURE__ */ new Set();
13342
+ const questTimeInfos = /* @__PURE__ */ new Map();
13343
+ filteredQuests.forEach((q) => {
13344
+ const dateProp = q.Date;
13345
+ let info = { isUndated: true, label: "" };
13346
+ if (dateProp) {
13347
+ if (dateProp.date) {
13348
+ const d = parseDate(dateProp.date);
13349
+ if (d) {
13350
+ info = { startDate: d, endDate: d, label: dateProp.date, isUndated: false };
13351
+ timePoints.add(d.getTime());
13352
+ }
13353
+ } else if (dateProp.year) {
13354
+ const y = parseInt(dateProp.year, 10);
13355
+ if (!isNaN(y)) {
13356
+ const d = new Date(y, 0, 1);
13357
+ info = { startDate: d, endDate: d, label: String(y), isUndated: false };
13358
+ timePoints.add(d.getTime());
13359
+ }
13360
+ } else if (dateProp.date_interval && dateProp.date_interval.start) {
13361
+ const sY = parseInt(dateProp.date_interval.start, 10);
13362
+ const eY = parseInt(dateProp.date_interval.end, 10) || sY;
13363
+ if (!isNaN(sY)) {
13364
+ const sd = new Date(sY, 0, 1);
13365
+ const ed = new Date(eY, 11, 31);
13366
+ info = { startDate: sd, endDate: ed, label: `${sY}-${eY}`, isUndated: false };
13367
+ timePoints.add(sd.getTime());
13368
+ timePoints.add(ed.getTime());
13369
+ }
13370
+ }
13371
+ }
13372
+ questTimeInfos.set(String(q.id), info);
13373
+ });
13374
+ const sortedTimePoints = Array.from(timePoints).sort((a, b) => a - b);
13375
+ const timeToYMap = /* @__PURE__ */ new Map();
13376
+ if (sortedTimePoints.length > 0) {
13377
+ const maxIdx = sortedTimePoints.length - 1;
13378
+ sortedTimePoints.forEach((t, i) => {
13379
+ timeToYMap.set(t, (i - maxIdx) * (QUEST_LENS_ROW_SPACING || 30));
13380
+ });
13381
+ }
13071
13382
  const startX = -((QUEST_STATUS_ORDER.length - 1) * QUEST_LENS_COLUMN_SPACING) / 2;
13072
13383
  Object.keys(nodeObjects).forEach((id) => {
13073
13384
  const mesh = nodeObjects[id];
@@ -13104,10 +13415,23 @@ function XViewScene({
13104
13415
  if (!mesh) {
13105
13416
  mesh = addOrUpdateNodeMesh(q, new THREE3.Vector3(colX, 150, 0), true);
13106
13417
  }
13107
- const targetY = (questsInCol.length - 1) * 0.5 * QUEST_LENS_ROW_SPACING - rowIndex * QUEST_LENS_ROW_SPACING;
13418
+ const timeInfo = questTimeInfos.get(nodeId) || { isUndated: true };
13419
+ let targetY;
13420
+ if (!timeInfo.isUndated) {
13421
+ targetY = timeToYMap.get(timeInfo.startDate.getTime());
13422
+ } else {
13423
+ const undatedOffset = sortedTimePoints.length > 0 ? sortedTimePoints.length * 5 : 0;
13424
+ targetY = -80 - rowIndex * (QUEST_LENS_ROW_SPACING || 30) - undatedOffset;
13425
+ }
13108
13426
  const lensTween = new Tween2(mesh.position).to({ x: colX, y: targetY, z: 0 }, 1500).easing(Easing2.Cubic.InOut);
13109
13427
  tweenGroup.add(lensTween);
13110
13428
  lensTween.start();
13429
+ if (!timeInfo.isUndated && timeInfo.label) {
13430
+ const dateSprite = createTextSprite(timeInfo.label, 36);
13431
+ dateSprite.name = "questDateLabel";
13432
+ dateSprite.position.set(0, 3.5, 0);
13433
+ mesh.add(dateSprite);
13434
+ }
13111
13435
  if (mesh.userData.labelObject) {
13112
13436
  const labelTargetPos = new THREE3.Vector3(colX, targetY, 0).add(
13113
13437
  mesh.userData.labelOffset || new THREE3.Vector3(0, -2.5, 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lv-x-software-house/x_view",
3
- "version": "1.2.5-dev.37",
3
+ "version": "1.2.5-dev.38",
4
4
  "description": "Pacote privado contendo os componentes e lógica de renderização 3D do X View.",
5
5
  "author": "iv.x - Engenharia de Software - ivxsoftwarehouse@gmail.com",
6
6
  "license": "UNLICENSED",