(Feat-Fix): Added unit display and completion in work allocation system, and ability to edit completed units. Fixed contractor reporting system not displaying data under corresponding departments.

This commit is contained in:
2025-12-31 08:09:04 +00:00
parent 1ac9d22e2d
commit 4e5555c658
13 changed files with 2361 additions and 29 deletions

View File

@@ -6,7 +6,7 @@ DB_NAME=work_allocation
DB_PORT=3307 DB_PORT=3307
# JWT Configuration - CHANGE IN PRODUCTION! # JWT Configuration - CHANGE IN PRODUCTION!
JWT_SECRET=work_alloc_jwt_secret_key_change_in_production_2024 JWT_SECRET=VwoPOwDth7aqCSlrbf/qYYY0upLu0kRuTcuM71ionJI=
JWT_EXPIRES_IN=7d JWT_EXPIRES_IN=7d
# Server Configuration # Server Configuration

View File

@@ -9,11 +9,27 @@
"@std/dotenv": "jsr:@std/dotenv@^0.225.3", "@std/dotenv": "jsr:@std/dotenv@^0.225.3",
"mysql2": "npm:mysql2@^3.11.0", "mysql2": "npm:mysql2@^3.11.0",
"bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts", "bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts",
"djwt": "https://deno.land/x/djwt@v3.0.2/mod.ts" "djwt": "https://deno.land/x/djwt@v3.0.2/mod.ts",
"react": "https://esm.sh/react@18",
"react-dom": "https://esm.sh/react-dom@18"
}, },
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true "strictNullChecks": true,
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"lint": {
"rules": {
"tags": ["recommended"]
}
},
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"semiColons": false
} }
} }

View File

@@ -69,9 +69,9 @@ router.get(
queryParams.push(endDate); queryParams.push(endDate);
} }
// Department filter (for SuperAdmin) // Department filter - filter by sub-department's department
if (departmentId && currentUser.role === "SuperAdmin") { if (departmentId) {
query += " AND e.department_id = ?"; query += " AND sd.department_id = ?";
queryParams.push(departmentId); queryParams.push(departmentId);
} }

View File

@@ -304,6 +304,91 @@ router.put(
}, },
); );
// Update work allocation units (Supervisor, Contractor, or SuperAdmin)
router.put(
"/:id/units",
authenticateToken,
authorize("Supervisor", "Contractor", "SuperAdmin"),
async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id;
const body = await ctx.request.body.json() as {
units?: number;
completedUnits?: number;
markComplete?: boolean;
};
const { units, completedUnits, markComplete } = body;
if (units === undefined && completedUnits === undefined) {
ctx.response.status = 400;
ctx.response.body = { error: "Units or completedUnits required" };
return;
}
// Verify allocation exists and user has access
let query = "SELECT * FROM work_allocations WHERE id = ?";
const params: unknown[] = [allocationId];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
} else if (currentUser.role === "Contractor") {
query += " AND contractor_id = ?";
params.push(currentUser.id);
}
const allocations = await db.query<WorkAllocation[]>(query, params);
if (allocations.length === 0) {
ctx.response.status = 403;
ctx.response.body = {
error: "Work allocation not found or access denied",
};
return;
}
const allocation = allocations[0];
const newUnits = units !== undefined ? units : allocation.units;
const newCompletedUnits = completedUnits !== undefined ? completedUnits : (allocation as any).completed_units || 0;
const rate = allocation.rate || 0;
const newTotalAmount = newCompletedUnits * rate;
// Determine status: mark as Completed if markComplete is true
const newStatus = markComplete ? "Completed" : allocation.status;
const completionDate = markComplete ? new Date().toISOString().split("T")[0] : allocation.completion_date;
await db.execute(
"UPDATE work_allocations SET units = ?, completed_units = ?, total_amount = ?, status = ?, completion_date = ? WHERE id = ?",
[newUnits, newCompletedUnits, newTotalAmount, newStatus, completionDate, allocationId],
);
const updatedAllocation = await db.query<WorkAllocation[]>(
`SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
c.name as contractor_name,
sd.name as sub_department_name,
d.name as department_name
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
JOIN users s ON wa.supervisor_id = s.id
JOIN users c ON wa.contractor_id = c.id
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`,
[allocationId],
);
ctx.response.body = updatedAllocation[0];
} catch (error) {
console.error("Update work allocation units error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Delete work allocation (Supervisor or SuperAdmin) // Delete work allocation (Supervisor or SuperAdmin)
router.delete( router.delete(
"/:id", "/:id",

View File

@@ -0,0 +1,15 @@
-- Add completed_units column to work_allocations table
-- Run this migration to support tracking completed units separately from total units
-- Note: MySQL 8.0 does not support IF NOT EXISTS for ADD COLUMN
-- Check if column exists before running:
-- SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'work_allocations' AND COLUMN_NAME = 'completed_units';
-- Add the column (will error if already exists)
ALTER TABLE work_allocations
ADD COLUMN completed_units DECIMAL(10,2) DEFAULT 0 AFTER units;
-- Update existing records to set completed_units equal to units for completed allocations
UPDATE work_allocations
SET completed_units = units
WHERE status = 'Completed' AND units IS NOT NULL;

View File

@@ -70,6 +70,7 @@ export interface WorkAllocation {
status: AllocationStatus; status: AllocationStatus;
rate: number | null; rate: number | null;
units: number | null; units: number | null;
completed_units: number | null;
total_amount: number | null; total_amount: number | null;
created_at: Date; created_at: Date;
employee_name?: string; employee_name?: string;

1972
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -82,6 +82,27 @@ export const useWorkAllocations = (
} }
}; };
const updateUnits = async (
id: number,
units?: number,
completedUnits?: number,
markComplete?: boolean,
) => {
setLoading(true);
setError(null);
try {
const updated = await api.updateWorkAllocationUnits(id, units, completedUnits, markComplete);
await fetchAllocations(); // Refresh list
return updated;
} catch (err: never) {
setError(err.message || "Failed to update work allocation units");
console.error("Failed to update work allocation units:", err);
throw err;
} finally {
setLoading(false);
}
};
return { return {
allocations, allocations,
loading, loading,
@@ -90,5 +111,6 @@ export const useWorkAllocations = (
createAllocation, createAllocation,
updateAllocation, updateAllocation,
deleteAllocation, deleteAllocation,
updateUnits,
}; };
}; };

View File

@@ -47,20 +47,19 @@ export const ContractorPaymentPage: React.FC = () => {
// Fetch report data // Fetch report data
const fetchReport = async () => { const fetchReport = async () => {
if (!selectedDepartment) {
alert("Please select a department");
return;
}
setLoading(true); setLoading(true);
try { try {
// Fetch completed allocations for the date range // Fetch completed allocations for the date range
const params: Record<string, any> = { const params: Record<string, any> = {
startDate, startDate,
endDate, endDate,
departmentId: parseInt(selectedDepartment),
}; };
// Only add departmentId if a specific department is selected
if (selectedDepartment) {
params.departmentId = parseInt(selectedDepartment);
}
const data = await api.getCompletedAllocationsReport(params); const data = await api.getCompletedAllocationsReport(params);
// Process data to group by sub-department and contractor // Process data to group by sub-department and contractor
@@ -113,14 +112,17 @@ export const ContractorPaymentPage: React.FC = () => {
const contractor = contractorMap.get(contractorId)!; const contractor = contractorMap.get(contractorId)!;
const amount = parseFloat(alloc.total_amount) || parseFloat(alloc.rate) || 0; const amount = parseFloat(alloc.total_amount) || parseFloat(alloc.rate) || 0;
const departmentName = (alloc.department_name || "").toLowerCase();
const subDeptNameLower = (alloc.sub_department_name || "").toLowerCase();
const activity = (alloc.activity || "").toLowerCase(); const activity = (alloc.activity || "").toLowerCase();
// Categorize by activity type // Categorize by department/sub-department name
if (activity.includes("dana")) { if (departmentName.includes("dana") || subDeptNameLower.includes("dana")) {
contractor.dana += amount; contractor.dana += amount;
} else if (activity.includes("tukdi")) { } else if (departmentName.includes("tudki") || departmentName.includes("tukdi") ||
subDeptNameLower.includes("tudki") || subDeptNameLower.includes("tukdi")) {
contractor.tukdi += amount; contractor.tukdi += amount;
} else if (activity.includes("groundnut")) { } else if (departmentName.includes("groundnut") || subDeptNameLower.includes("groundnut")) {
contractor.groundnut += amount; contractor.groundnut += amount;
} else if (activity.includes("commission") || activity.includes("salary")) { } else if (activity.includes("commission") || activity.includes("salary")) {
contractor.commission_salary += amount; contractor.commission_salary += amount;
@@ -383,7 +385,7 @@ export const ContractorPaymentPage: React.FC = () => {
onChange={(e) => setSelectedDepartment(e.target.value)} onChange={(e) => setSelectedDepartment(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
> >
<option value="">Select Department</option> <option value="">All Departments</option>
{departments.map((dept) => ( {departments.map((dept) => (
<option key={dept.id} value={dept.id}>{dept.name}</option> <option key={dept.id} value={dept.id}>{dept.name}</option>
))} ))}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { CheckCircle, Plus, RefreshCw, Search, Trash2 } from "lucide-react"; import { CheckCircle, Edit2, Plus, RefreshCw, Search, Trash2, X } from "lucide-react";
import { Card, CardContent } from "../components/ui/Card.tsx"; import { Card, CardContent } from "../components/ui/Card.tsx";
import { import {
Table, Table,
@@ -31,6 +31,7 @@ export const WorkAllocationPage: React.FC = () => {
createAllocation, createAllocation,
updateAllocation, updateAllocation,
deleteAllocation, deleteAllocation,
updateUnits,
} = useWorkAllocations(); } = useWorkAllocations();
const { departments } = useDepartments(); const { departments } = useDepartments();
const { employees } = useEmployees(); const { employees } = useEmployees();
@@ -39,6 +40,15 @@ export const WorkAllocationPage: React.FC = () => {
// Check if user is supervisor (limited to their department) // Check if user is supervisor (limited to their department)
const isSupervisor = user?.role === "Supervisor"; const isSupervisor = user?.role === "Supervisor";
// Check if user can edit units (supervisor, contractor, or super admin)
const canEditUnits = user?.role === "Supervisor" || user?.role === "Contractor" || user?.role === "SuperAdmin";
// State for edit units modal
const [editingAllocation, setEditingAllocation] = useState<any | null>(null);
const [editUnitsValue, setEditUnitsValue] = useState<string>("");
const [editCompletedUnitsValue, setEditCompletedUnitsValue] = useState<string>("");
const [markCompleteChecked, setMarkCompleteChecked] = useState<boolean>(false);
// Get supervisor's department name // Get supervisor's department name
const supervisorDeptName = const supervisorDeptName =
@@ -213,6 +223,42 @@ export const WorkAllocationPage: React.FC = () => {
} }
}; };
const handleOpenEditUnits = (allocation: any, fromCheckmark = false) => {
setEditingAllocation(allocation);
setEditUnitsValue(String(allocation.units || 0));
setEditCompletedUnitsValue(String(allocation.completed_units || 0));
// If opened from checkmark, default to marking complete
setMarkCompleteChecked(fromCheckmark && allocation.status !== "Completed");
};
const handleCloseEditUnits = () => {
setEditingAllocation(null);
setEditUnitsValue("");
setEditCompletedUnitsValue("");
setMarkCompleteChecked(false);
};
const handleSaveUnits = async () => {
if (!editingAllocation) return;
try {
const units = parseFloat(editUnitsValue) || 0;
const completedUnits = parseFloat(editCompletedUnitsValue) || 0;
await updateUnits(editingAllocation.id, units, completedUnits, markCompleteChecked);
handleCloseEditUnits();
} catch (err: any) {
alert(err.message || "Failed to update units");
}
};
const handleMarkCompleteWithUnits = async (allocation: any) => {
const isPerUnit = allocation.units && allocation.units > 0;
if (isPerUnit && canEditUnits) {
handleOpenEditUnits(allocation, true);
} else {
await handleMarkComplete(allocation.id);
}
};
// Calculate summary stats // Calculate summary stats
const stats = { const stats = {
total: allocations.length, total: allocations.length,
@@ -517,8 +563,10 @@ export const WorkAllocationPage: React.FC = () => {
<TableHead>ID</TableHead> <TableHead>ID</TableHead>
<TableHead>Employee</TableHead> <TableHead>Employee</TableHead>
<TableHead>Contractor</TableHead> <TableHead>Contractor</TableHead>
<TableHead>Department</TableHead>
<TableHead>Sub-Department</TableHead> <TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead> <TableHead>Activity</TableHead>
<TableHead>Units</TableHead>
<TableHead>Date</TableHead> <TableHead>Date</TableHead>
<TableHead>Rate Details</TableHead> <TableHead>Rate Details</TableHead>
<TableHead>Status</TableHead> <TableHead>Status</TableHead>
@@ -526,12 +574,12 @@ export const WorkAllocationPage: React.FC = () => {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredAllocations.map((allocation) => { {filteredAllocations.map((allocation) => {
const isPerUnit = allocation.activity === "Loading" || const isPerUnit = allocation.units && allocation.units > 0;
allocation.activity === "Unloading";
const units = parseFloat(allocation.units) || 0; const units = parseFloat(allocation.units) || 0;
const completedUnits = parseFloat(allocation.completed_units) || 0;
const rate = parseFloat(allocation.rate) || 0; const rate = parseFloat(allocation.rate) || 0;
const total = parseFloat(allocation.total_amount) || const total = parseFloat(allocation.total_amount) ||
(isPerUnit ? units * rate : rate); (isPerUnit ? completedUnits * rate : rate);
return ( return (
<TableRow key={allocation.id}> <TableRow key={allocation.id}>
@@ -542,6 +590,9 @@ export const WorkAllocationPage: React.FC = () => {
<TableCell> <TableCell>
{allocation.contractor_name || "-"} {allocation.contractor_name || "-"}
</TableCell> </TableCell>
<TableCell>
{allocation.department_name || "-"}
</TableCell>
<TableCell> <TableCell>
{allocation.sub_department_name || "-"} {allocation.sub_department_name || "-"}
</TableCell> </TableCell>
@@ -550,8 +601,7 @@ export const WorkAllocationPage: React.FC = () => {
? ( ? (
<span <span
className={`px-2 py-1 rounded text-xs font-medium ${ className={`px-2 py-1 rounded text-xs font-medium ${
allocation.activity === "Loading" || isPerUnit
allocation.activity === "Unloading"
? "bg-purple-100 text-purple-700" ? "bg-purple-100 text-purple-700"
: "bg-gray-100 text-gray-700" : "bg-gray-100 text-gray-700"
}`} }`}
@@ -561,6 +611,41 @@ export const WorkAllocationPage: React.FC = () => {
) )
: "-"} : "-"}
</TableCell> </TableCell>
<TableCell>
{isPerUnit ? (
<div className="flex items-center gap-2">
<div className="text-sm">
<span className={`font-semibold ${completedUnits < units ? "text-orange-600" : "text-green-600"}`}>
{completedUnits}
</span>
<span className="text-gray-500"> / {units}</span>
{completedUnits < units && (
<div className="text-xs text-orange-500">
({units - completedUnits} remaining)
</div>
)}
{completedUnits >= units && units > 0 && (
<div className="text-xs text-green-500">
Complete
</div>
)}
</div>
{canEditUnits && (
<Button
variant="ghost"
size="sm"
onClick={() => handleOpenEditUnits(allocation)}
className="text-blue-600 p-1"
title="Edit Units"
>
<Edit2 size={12} />
</Button>
)}
</div>
) : (
<span className="text-gray-400">-</span>
)}
</TableCell>
<TableCell> <TableCell>
{new Date(allocation.assigned_date) {new Date(allocation.assigned_date)
.toLocaleDateString()} .toLocaleDateString()}
@@ -569,11 +654,11 @@ export const WorkAllocationPage: React.FC = () => {
{rate > 0 {rate > 0
? ( ? (
<div className="text-sm"> <div className="text-sm">
{isPerUnit && units > 0 {isPerUnit && completedUnits > 0
? ( ? (
<div> <div>
<div className="text-gray-500"> <div className="text-gray-500">
{units} × {rate.toFixed(2)} {completedUnits} × {rate.toFixed(2)}
</div> </div>
<div className="font-semibold text-green-600"> <div className="font-semibold text-green-600">
= {total.toFixed(2)} = {total.toFixed(2)}
@@ -611,9 +696,9 @@ export const WorkAllocationPage: React.FC = () => {
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onClick={() =>
handleMarkComplete(allocation.id)} handleMarkCompleteWithUnits(allocation)}
className="text-green-600" className="text-green-600"
title="Mark Complete" title={isPerUnit ? "Complete & Set Units" : "Mark Complete"}
> >
<CheckCircle size={14} /> <CheckCircle size={14} />
</Button> </Button>
@@ -691,6 +776,123 @@ export const WorkAllocationPage: React.FC = () => {
)} )}
</CardContent> </CardContent>
</Card> </Card>
{/* Edit Units Modal */}
{editingAllocation && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md shadow-xl">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-gray-800">
Edit Units - {editingAllocation.activity}
</h3>
<button
type="button"
onClick={handleCloseEditUnits}
className="text-gray-500 hover:text-gray-700"
>
<X size={20} />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Total Units Assigned
</label>
<input
type="number"
value={editUnitsValue}
onChange={(e) => setEditUnitsValue(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min="0"
step="1"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Completed Units
</label>
<input
type="number"
value={editCompletedUnitsValue}
onChange={(e) => setEditCompletedUnitsValue(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min="0"
step="1"
/>
</div>
{/* Units Difference Display */}
{(() => {
const totalUnits = parseFloat(editUnitsValue) || 0;
const completed = parseFloat(editCompletedUnitsValue) || 0;
const remaining = totalUnits - completed;
return (
<div className={`p-3 rounded-md ${remaining > 0 ? "bg-orange-50 border border-orange-200" : "bg-green-50 border border-green-200"}`}>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Total Units:</span>
<span className="font-medium">{totalUnits}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Completed:</span>
<span className="font-medium text-green-600">{completed}</span>
</div>
{remaining > 0 && (
<div className="flex justify-between text-sm">
<span className="text-gray-600">Remaining:</span>
<span className="font-medium text-orange-600">{remaining}</span>
</div>
)}
{remaining === 0 && totalUnits > 0 && (
<div className="text-sm text-green-600 font-medium mt-1">
All units completed
</div>
)}
</div>
);
})()}
{/* Calculation Preview */}
{parseFloat(editingAllocation.rate) > 0 && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md">
<div className="text-sm text-gray-600">
Rate: {parseFloat(editingAllocation.rate).toFixed(2)} per unit
</div>
<div className="text-lg font-semibold text-green-600 mt-1">
Total Payment: {((parseFloat(editCompletedUnitsValue) || 0) * parseFloat(editingAllocation.rate)).toFixed(2)}
</div>
</div>
)}
{/* Mark as Complete Checkbox */}
{editingAllocation.status !== "Completed" && (
<div className="flex items-center gap-2">
<input
type="checkbox"
id="markComplete"
checked={markCompleteChecked}
onChange={(e) => setMarkCompleteChecked(e.target.checked)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="markComplete" className="text-sm font-medium text-gray-700">
Mark as Completed
</label>
</div>
)}
</div>
<div className="flex justify-end gap-3 mt-6">
<Button variant="ghost" onClick={handleCloseEditUnits}>
Cancel
</Button>
<Button variant="primary" onClick={handleSaveUnits}>
{markCompleteChecked ? "Save & Complete" : "Save Units"}
</Button>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@@ -174,6 +174,18 @@ class ApiService {
}); });
} }
updateWorkAllocationUnits(
id: number,
units?: number,
completedUnits?: number,
markComplete?: boolean,
) {
return this.request<any>(`/work-allocations/${id}/units`, {
method: "PUT",
body: JSON.stringify({ units, completedUnits, markComplete }),
});
}
// Attendance // Attendance
getAttendance( getAttendance(
params?: { params?: {

View File

@@ -56,11 +56,15 @@ export interface WorkAllocation {
supervisor_id: number; supervisor_id: number;
contractor_id: number; contractor_id: number;
sub_department_id?: number; sub_department_id?: number;
activity?: string;
description?: string; description?: string;
assigned_date: string; assigned_date: string;
status: "Pending" | "InProgress" | "Completed" | "Cancelled"; status: "Pending" | "InProgress" | "Completed" | "Cancelled";
completion_date?: string; completion_date?: string;
rate?: number; rate?: number;
units?: number;
completed_units?: number;
total_amount?: number;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
employee_name?: string; employee_name?: string;

View File

@@ -452,7 +452,7 @@ export const exportReportToXLSX = (
})); }));
const ws3 = XLSX.utils.json_to_sheet(contractorData); const ws3 = XLSX.utils.json_to_sheet(contractorData);
ws3["!cols"] = [ ws3["!cols"] = [.
{ wch: 25 }, // Contractor { wch: 25 }, // Contractor
{ wch: 12 }, // Employees { wch: 12 }, // Employees
{ wch: 16 }, // Total Completed { wch: 16 }, // Total Completed
@@ -466,7 +466,8 @@ export const exportReportToXLSX = (
? `${dateRange.startDate}_to_${dateRange.endDate}` ? `${dateRange.startDate}_to_${dateRange.endDate}`
: new Date().toISOString().split("T")[0]; : new Date().toISOString().split("T")[0];
const deptStr = departmentName.toLowerCase().replace(/\s+/g, "_"); const deptStr = departmentName.toLowerCase().replace(/\s+/g, "_");
const filename = `work_report_${deptStr}_${dateStr}.xlsx`; const randomStr = Math.random().toString(36).substring(2, 11);
const filename = `work_report${randomStr}_${deptStr}_${dateStr}.xlsx`;
// Write and download // Write and download
XLSX.writeFile(wb, filename); XLSX.writeFile(wb, filename);