first commit
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
TextButton,
|
||||
FormComboboxInput,
|
||||
Input,
|
||||
Button
|
||||
} from "ikoncomponents";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export default function ResourceModal({ tasks, setResourceModal ,addMultipleRows}: { tasks: any[], setResourceModal: any, addMultipleRows: any }) {
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
rows: 1,
|
||||
selectAll: false,
|
||||
sheets: []
|
||||
}
|
||||
});
|
||||
const { register, watch, setValue } = form;
|
||||
|
||||
const selectAll = watch("selectAll");
|
||||
const sheets = watch("sheets");
|
||||
const tasksLength = tasks?.length || 0;
|
||||
|
||||
// 1. Single Effect: Sync "Select All" checkbox UI when individual items change
|
||||
useEffect(() => {
|
||||
const allSelected = sheets.length === tasksLength && tasksLength > 0;
|
||||
// Use { shouldValidate: true } if you have validation rules
|
||||
setValue("selectAll", allSelected);
|
||||
}, [sheets, tasksLength, setValue]);
|
||||
|
||||
// 2. Manual Handler: When "Select All" is clicked
|
||||
const handleSelectAllChange = (e) => {
|
||||
const isChecked = e.target.checked;
|
||||
setValue("selectAll", isChecked);
|
||||
|
||||
if (isChecked) {
|
||||
setValue("sheets", tasks.map((t) => t.taskName));
|
||||
} else {
|
||||
setValue("sheets", []);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnSubmit = async (data) => {
|
||||
console.log(data);
|
||||
// for(var i in data.sheets)
|
||||
await addMultipleRows(data.rows, data.sheets);
|
||||
setResourceModal(false);
|
||||
};
|
||||
|
||||
const setClose = () => {
|
||||
// Logic to close the modal
|
||||
form.reset(); // Reset form values when closing
|
||||
setResourceModal(false);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={() => {setClose()}}>
|
||||
<DialogContent className="w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold">Add multiple rows</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleOnSubmit)}
|
||||
id="resource-form"
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* Row count */}
|
||||
<div className="flex gap-4 flex-col ">
|
||||
<label className="font-medium ">
|
||||
No. of Row(s) to be Inserted
|
||||
</label>
|
||||
{/* <Input
|
||||
form={form}
|
||||
placeholder="No. of Row(s) to be Inserted"
|
||||
name="rows"
|
||||
type="number"
|
||||
/> */}
|
||||
<Input
|
||||
type="number"
|
||||
{...form.register("rows")}
|
||||
className="border rounded px-2 py-1 "
|
||||
/>
|
||||
</div>
|
||||
<hr className="border-gray-200" />
|
||||
{/* Select All */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" {...form.register("selectAll")} onChange={handleSelectAllChange}/>
|
||||
<label>Select All</label>
|
||||
</div>
|
||||
|
||||
{/* Sheet list */}
|
||||
<div className="pl-6 space-y-2">
|
||||
{tasks.map((task) => (
|
||||
<div key={task.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={task.taskName}
|
||||
{...form.register("sheets")}
|
||||
/>
|
||||
<label>{task.taskName}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button variant={"outline"} type="submit" form="resource-form">
|
||||
Add
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
import moment from "moment";
|
||||
|
||||
// types.ts (Optional but recommended)
|
||||
export interface Cell {
|
||||
index: number;
|
||||
value?: string | number;
|
||||
}
|
||||
|
||||
export interface Row {
|
||||
index: number;
|
||||
cells: Cell[];
|
||||
}
|
||||
|
||||
export interface ResourceAllocation {
|
||||
allocation: Record<string, number>;
|
||||
detailedAllocation: Record<string, any>;
|
||||
resourceType: string;
|
||||
role: string;
|
||||
gradeId: number | string | null;
|
||||
employeeName: string;
|
||||
taskName: string;
|
||||
resourceId: string;
|
||||
taskId: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the spreadsheet-like ds1 structure into the flat ds2 allocation list.
|
||||
*/
|
||||
export function transformDs1ToDs2(ds1: any[]){
|
||||
if (!ds1 || ds1.length === 0) return [];
|
||||
|
||||
const rows = ds1[0].rows;
|
||||
const ds2: any[] = [];
|
||||
|
||||
// 1. Map columns for months from header (Index 0)
|
||||
const monthMap: Record<number, string> = {};
|
||||
rows[0].cells.forEach((cell: any) => {
|
||||
if (cell.index >= 5 && typeof cell.value === "string") {
|
||||
monthMap[cell.index] = cell.value.replace(/\s+/g, "_");
|
||||
}
|
||||
});
|
||||
|
||||
// Variables to track the current task context
|
||||
let currentTaskName = "";
|
||||
let currentTaskId: number | null = null;
|
||||
|
||||
rows.slice(1).forEach((row: any) => {
|
||||
const cells = row.cells;
|
||||
|
||||
// Extract Index 0 (Column A) - Used for Task context or Subtotal checks
|
||||
const colA = cells.find((c: any) => c.index === 0)?.value?.toString() || "";
|
||||
// Extract Index 1 (Column B) - Staff/Employee
|
||||
const colB = cells.find((c: any) => c.index === 1)?.value?.toString() || "";
|
||||
|
||||
|
||||
// --- LOGIC A: Detect Group Header (Task Info) ---
|
||||
// If it starts with '˄', it defines the Task for the following rows
|
||||
if (colA.startsWith("˄")) {
|
||||
currentTaskName = colA.replace("˄", "").trim();
|
||||
// Using row index or a hash as a placeholder for taskId if not explicitly in ds1
|
||||
currentTaskId = cells[1]?.textAlign; // Example heuristic for taskId
|
||||
return;
|
||||
}
|
||||
|
||||
// --- LOGIC B: Skip Totals/Subtotals ---
|
||||
if (colA.toLowerCase().includes("subtotal") || colA.toLowerCase() === "total" || !colB) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- LOGIC C: Process Resource Row ---
|
||||
// Handle "Abhishek Show - K2408137" splitting
|
||||
let employeeName = colB;
|
||||
let resourceId = "";
|
||||
|
||||
if (colB.includes("-")) {
|
||||
const parts = colB.split("-");
|
||||
employeeName = parts[0].trim();
|
||||
resourceId = parts[1].trim();
|
||||
}
|
||||
|
||||
const record: any = {
|
||||
allocation: {},
|
||||
detailedAllocation: {},
|
||||
resourceType: "Named",
|
||||
role: cells.find((c: any) => c.index === 3)?.value || "",
|
||||
gradeId: cells.find((c: any) => c.index === 2)?.value || null,
|
||||
employeeName: employeeName,
|
||||
resourceId: resourceId,
|
||||
taskName: currentTaskName, // Taken from the last seen Group Header
|
||||
taskId: currentTaskId, // Taken from the last seen Group Header
|
||||
};
|
||||
|
||||
// Extract allocations
|
||||
cells.forEach((cell: any) => {
|
||||
if (monthMap[cell.index]) {
|
||||
const monthKey = monthMap[cell.index];
|
||||
if (typeof cell.value === "number") {
|
||||
record.allocation[monthKey] = cell.value;
|
||||
}
|
||||
record.detailedAllocation[monthKey] = {};
|
||||
}
|
||||
});
|
||||
|
||||
ds2.push(record);
|
||||
});
|
||||
|
||||
return ds2;
|
||||
}
|
||||
|
||||
export function transformDataToSheet(ds2: any[]) {
|
||||
if (!ds2 || ds2.length === 0) return {};
|
||||
|
||||
return ds2.reduce((acc: any, curr: any) => {
|
||||
const { taskName, employeeName, resourceId, gradeId, role, allocation, id } = curr;
|
||||
|
||||
// Initialize task group if it doesn't exist
|
||||
if (!acc[taskName]) {
|
||||
acc[taskName] = [];
|
||||
}
|
||||
|
||||
// Push simplified resource object
|
||||
acc[taskName].push({
|
||||
staff: `${employeeName} - ${resourceId}`,
|
||||
grade: gradeId,
|
||||
role: role,
|
||||
id: id,
|
||||
...allocation // Spreads monthwise FTE (e.g., Dec_2025: 0.12)
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export async function addMultipleRows(spreadsheetRef: any, rows: number, tasks: {}, taskCells: {}, taskMonths: []) {
|
||||
|
||||
let newTaskCellsWithHeader = { headerTitle: "", taskCells: taskCells };
|
||||
for (var t = 0; t < tasks.length; t++) {
|
||||
|
||||
|
||||
|
||||
console.log("Batching multiple row addition");
|
||||
for (var r = 0; r < rows; r++) {
|
||||
let subtotalRow = 0;
|
||||
for (var tc of newTaskCellsWithHeader.taskCells) {
|
||||
if (tc.headerTitle == tasks[t]) {
|
||||
subtotalRow = tc.subtotalRow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (subtotalRow == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// await addRowBelow(spreadsheetRef, {row: , taskCells: taskCells})
|
||||
const ss = spreadsheetRef.current;
|
||||
const sheet = ss.activeSheet();
|
||||
const row = subtotalRow - 2;
|
||||
|
||||
|
||||
// Insert row below
|
||||
sheet.insertRow(row + 1);
|
||||
|
||||
|
||||
const data = (await ss.saveJSON())?.sheets;
|
||||
const rows = data[0].rows
|
||||
let newRows = []
|
||||
rows.forEach((element, i) => {
|
||||
if (i == row) {
|
||||
let elm = structuredClone(element)
|
||||
const frm = element.cells[4].formula;
|
||||
element.cells[4].value = 0;
|
||||
element.cells[4].formula = frm?.replaceAll(i + 1 + "", i + 2 + "")
|
||||
// element.cells[4].formula = frm.replaceAll(row+1+"",row+2+"" )
|
||||
if (element.cells[5].validation)
|
||||
element.cells[5].validation.from = element.cells[5].validation.from.replaceAll(row + 1 + "", row + 2 + "")
|
||||
|
||||
for (var t in taskMonths) {
|
||||
element.cells[5 + parseInt(t)].value = ""
|
||||
element.cells[5 + parseInt(t)].enable = false
|
||||
}
|
||||
|
||||
newRows.push(elm);
|
||||
}
|
||||
if (i >= row) {
|
||||
element.index = i + 1;
|
||||
if (i == row) {
|
||||
element.cells[1].value = ""
|
||||
element.cells[2].value = ""
|
||||
element.cells[3].value = ""
|
||||
}
|
||||
|
||||
}
|
||||
newRows.push(element); // 👈 keep existing row
|
||||
});
|
||||
data[0].rows = newRows
|
||||
ss.fromJSON({ sheets: data })
|
||||
|
||||
const totalCols = sheet._columns._count;
|
||||
|
||||
newTaskCellsWithHeader = await updateHeaderAndSubtotal(newTaskCellsWithHeader.taskCells, row)
|
||||
// setTaskCells(newTaskCellsWithHeader.taskCells)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// subtotalRow++;
|
||||
}
|
||||
|
||||
for (var i of newTaskCellsWithHeader.taskCells) {
|
||||
const sheet = spreadsheetRef.current.activeSheet();
|
||||
const monthsArr = [4]
|
||||
taskMonths.forEach((t, i) => { monthsArr.push(i + 5) })
|
||||
// if(i.headerTitle = taskName){
|
||||
monthsArr.forEach((t) => {
|
||||
|
||||
const cell = sheet.range(i.subtotalRow - 1, t);
|
||||
let sumF = ""
|
||||
for (let v = i.headerRow + 1; v < i.subtotalRow; v++) {
|
||||
if (sumF == "") {
|
||||
sumF += columnName(t) + v;
|
||||
continue
|
||||
}
|
||||
sumF += "," + columnName(t) + v
|
||||
}
|
||||
// sumF.slice(sumF.lastIndexOf(","), sumF.length)
|
||||
// let formula = cell.formula();
|
||||
// formula = formula.replaceAll(formula.charAt(5),Number(formula.charAt(5))-1)
|
||||
cell.formula(`SUM(${sumF})`);
|
||||
|
||||
|
||||
})
|
||||
// const row = sheet.range(i.subtotalRow, c);
|
||||
// cells = .map(r => `E${r}`).join(",")
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
return newTaskCellsWithHeader;
|
||||
|
||||
}
|
||||
|
||||
export async function updateHeaderAndSubtotal(taskCells = [], row = 0) {
|
||||
// 1. Find the target section
|
||||
const targetIndex = taskCells.findIndex(
|
||||
({ headerRow, subtotalRow }) => row >= headerRow && row <= subtotalRow
|
||||
);
|
||||
|
||||
// If no section found, return the original data structure
|
||||
if (targetIndex === -1) {
|
||||
return { headerTitle: null, taskCells };
|
||||
}
|
||||
|
||||
const targetName = taskCells[targetIndex].headerTitle;
|
||||
|
||||
// 2. Map through the array to create a NEW updated version
|
||||
const updated = taskCells.map((item, index) => {
|
||||
// If it's the section we are currently in, increment the subtotal
|
||||
if (index === targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
subtotalRow: item.subtotalRow + 1,
|
||||
};
|
||||
}
|
||||
|
||||
// If it's a section AFTER the current one, shift everything down by 1
|
||||
if (index > targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
headerRow: item.headerRow + 1,
|
||||
subtotalRow: item.subtotalRow + 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, leave it as is
|
||||
return item;
|
||||
});
|
||||
|
||||
return { headerTitle: targetName, taskCells: updated };
|
||||
}
|
||||
|
||||
export function getTaskTimeline(tasks: any[]) {
|
||||
if (!tasks || tasks.length === 0) return [];
|
||||
|
||||
// 1. Calculate End Dates for all tasks
|
||||
const tasksWithEnds = tasks.map(t => {
|
||||
const taskStart = moment(t.taskStart);
|
||||
const taskDuration = parseFloat(t.taskDuration);
|
||||
|
||||
// Calculate logical end date
|
||||
let endDate = taskStart.clone().add(Math.floor(taskDuration), "months");
|
||||
const daysInLastMonth = endDate.daysInMonth();
|
||||
|
||||
endDate.subtract(1, "day");
|
||||
endDate.add(daysInLastMonth * (taskDuration % 1), "days");
|
||||
|
||||
if (endDate.isBefore(taskStart)) endDate = taskStart.clone();
|
||||
|
||||
return {
|
||||
...t,
|
||||
start: taskStart,
|
||||
end: endDate
|
||||
};
|
||||
});
|
||||
|
||||
// 2. Define the global range based on tasks
|
||||
const minStart = moment.min(tasksWithEnds.map(t => t.start)).clone().startOf('month');
|
||||
const maxEnd = moment.max(tasksWithEnds.map(t => t.end)).clone().endOf('month');
|
||||
|
||||
const timeline = [];
|
||||
let currentMoment = minStart.clone();
|
||||
|
||||
// 3. Iterate through each month
|
||||
while (currentMoment.isSameOrBefore(maxEnd, 'month')) {
|
||||
const monthStart = currentMoment.clone().startOf('month');
|
||||
const monthEnd = currentMoment.clone().endOf('month');
|
||||
const daysInMonth = currentMoment.daysInMonth();
|
||||
|
||||
// Filter and calculate duration for tasks in THIS specific month
|
||||
const monthTasks = tasksWithEnds
|
||||
.filter(t => t.start.isSameOrBefore(monthEnd) && t.end.isSameOrAfter(monthStart))
|
||||
.map(t => {
|
||||
const overlapStart = moment.max(t.start, monthStart);
|
||||
const overlapEnd = moment.min(t.end, monthEnd);
|
||||
|
||||
const overlapDays = overlapEnd.diff(overlapStart, 'days') + 1;
|
||||
const taskMonthDuration = parseFloat((overlapDays / daysInMonth).toFixed(2));
|
||||
|
||||
return {
|
||||
taskName: t.taskName,
|
||||
taskDuration: taskMonthDuration
|
||||
};
|
||||
});
|
||||
|
||||
// Generate the month name string as requested: "Jan 2024"
|
||||
const dateObj = currentMoment.toDate();
|
||||
const monthName = dateObj.toLocaleString("en-US", { month: "long" });
|
||||
const year = dateObj.getFullYear();
|
||||
|
||||
timeline.push({
|
||||
month: `${monthName.slice(0, 3)} ${year}`,
|
||||
tasks: monthTasks
|
||||
});
|
||||
|
||||
currentMoment.add(1, 'month');
|
||||
}
|
||||
|
||||
return timeline;
|
||||
}
|
||||
|
||||
function columnName(n) {
|
||||
let s = "";
|
||||
while (n >= 0) {
|
||||
s = String.fromCharCode((n % 26) + 65) + s;
|
||||
n = Math.floor(n / 26) - 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
Reference in New Issue
Block a user