Send Email Template with Variables From Excel Using Macro (and maybe java??) - java

Big but maybe simple help is needed!
I am using excel on Windows 10 with Outlook to send emails.
My worksheet looks as per image, where I have some inputs (example email, item, company...) and a cell called "Trigger Column (templates)" from where I set a macro in which each time I select a new template from the dropdown list, the "Status" column will change from "sent!" in "to be sent".
What I want to do not is to send ONLY the templates that have in status "to be sent!".
Other than "Worksheet", I have more sheets called "Sender" and "Templates".
As you can see on "Templates" sheet I have various columns. Under the column "Template content" I have some variables that can be found in the sheet "Worksheet":
Hi <Name>,
I hope you are well.
We would like to order from you <Quantity> <Color> <Item> at the agreed price of <Price> EUR.
We need to receive the order no later than <Deadline>.
<Signature>
What I would really love to do is to be able to send only to the rows marked as "to be sent!" the email as per the template, replacing any variable with the correct data.
What currently have in the Marco is:
Sub CommandButton1_Click()
ActiveWorkbook.Save
Dim sh As Worksheet
Dim rw As Range
Set sh = ActiveSheet
Dim oShell
Dim oExec
Set oShell = CreateObject("WScript.Shell")
rowIndex = 0
statusIndex = getColIndexByTitle("Status")
script2 = "javaw -jar " + """" + Application.ActiveWorkbook.Path + "/SupplierMailSender.jar" + """ " + """" + Application.ActiveWorkbook.FullName + """"
Set oExec = oShell.Exec(script2)
Debug.Print script2
x = oExec.StdOut.ReadLine
Set ws = Application.ActiveSheet
Dim WrdArray() As String
WrdArray() = Split(x)
For i = LBound(WrdArray) To UBound(WrdArray)
rowIndex = CInt(WrdArray(i))
ws.Cells(rowIndex, CInt(statusIndex)).Value = "sent!"
Next i
Debug.Print x
ActiveWorkbook.Save
End Sub
Public Function getColIndexByTitle(rowName As String) As Integer
titleRowIndex = 1
col = 1
Set ws = Application.ActiveSheet
Do While True
If ws.Cells(titleRowIndex, col).Value = "" Then
Exit Do
End If
If ws.Cells(titleRowIndex, col).Value = rowName Then
getColIndexByTitle = col
Exit Do
End If
col = col + 1
Loop
End Function
Private Sub Worksheet_Change(ByVal Target As Range)
statusCol = getColIndexByTitle("Status")
templateCol = getColIndexByTitle("Trigger Column (templates)")
If (templateCol = Target.Column) Then
For Each cel In Target ' do the next steps for each cell that was changed
Set ws = Application.ActiveSheet
ws.Cells(cel.Row, statusCol).Value = "to be sent!"
Next cel
End If
End Sub
The problem is that I took from an example this but I do not have:
script2 = "javaw -jar " + """" + Application.ActiveWorkbook.Path + "/SupplierMailSender.jar" + """ " + """" + Application.ActiveWorkbook.FullName + """"
Set oExec = oShell.Exec(script2)
Debug.Print script2
x = oExec.StdOut.ReadLine
I guess this is the part that should be replaced to create the Outlook email. I am unsure if I can send it directly from VBA or if the email needs a Java file. In that case, I do not know Java code and I need some help to send the email correctly. I really hope an Angel can help!!!

Related

Marklogic Aggregate query with Java API

I am trying to get an aggregate query to run using the ML Java API, and am having a bit of trouble. I have followed the documentation, but there is a requirement for a values constraint, and i'm not really sure what that is supposed to look like. I tried the following:
String options =
"<options xmlns:search=\"http://marklogic.com/appservices/search\">" +
" <values name=\"average\">" +
" <range type=\"xs:string\">" +
" <element ns=\"\" name=\"content-id\"/>" +
" </range>" +
" </values>" +
"</options>";
StringHandle handle = new StringHandle(options);
QueryOptionsManager optMgr = client.newServerConfigManager().newQueryOptionsManager();
optMgr.writeOptions("average", handle);
QueryManager queryManager = client.newQueryManager();
StructuredQueryBuilder queryBuilder = queryManager.newStructuredQueryBuilder();
ValuesDefinition valuesDefinition = queryManager.newValuesDefinition("average");
valuesDefinition.setAggregate("avg");
valuesDefinition.setQueryDefinition(queryBuilder.value(queryBuilder.element("content-id"),contentId));
ValuesHandle results = queryManager.values(valuesDefinition, new ValuesHandle());
I took a stab at the options based on some other options i'm using. However, when I try to write the options it tells me Invalid Content: Unexpected Payload.
I get the feeling i'm going about this the wrong way. Essentially I want to find all documents that have a given value in the element "content-id", and then get the average of another element called "star-rating".
Should the options be set for "content-id" or "star-rating"? The documentation doesn't show the use of a queryDefinition, should I remove that? Modify it? Is there an easier way to do this in Java?
Edit: Forgot to mention, I also created an element range index on content-id with type string.
With the guidance of #SamMefford I was able to reach a solution. It looks like this:
String options =
"<options xmlns=\"http://marklogic.com/appservices/search\">" +
" <values name=\"star-rating\">" +
" <range type=\"xs:float\">" +
" <element ns=\"\" name=\"star-rating\"/>" +
" </range>" +
" </values>" +
"</options>";
StringHandle handle = new StringHandle(options);
QueryOptionsManager optMgr = client.newServerConfigManager().newQueryOptionsManager();
optMgr.writeOptions("star-rating", handle);
QueryManager queryManager = client.newQueryManager();
StructuredQueryBuilder queryBuilder =
queryManager.newStructuredQueryBuilder("star-rating");
ValuesDefinition valuesDefinition = queryManager.newValuesDefinition("star-rating");
valuesDefinition.setAggregate("avg");
valuesDefinition.setQueryDefinition(
queryBuilder.range(
queryBuilder.element("content-id"),"string",
StructuredQueryBuilder.Operator.EQ,contentId
)
);
String results = queryManager.values(valuesDefinition, new ValuesHandle())
.getAggregate("avg").getValue();
The options were created around the field that I wanted the average of. I created element indexes for both < star-rating > and < content-id >. The query then allowed me to filter to records with a specific content-id, and then get the average value of their star-ratings.

Export multiple Tables from Excel to a template Word document using VBA/Apache POI

So I have this macro, which can copy a single table from an Excel sheet to a template Word document. How do I modify it, so it can copy let's say 6 tables from 6 different Excel sheets into a template Word document? Or can I achieve this with Apache POI API to copy multiple tables from Excel to Word??
Sub ExportDataToWord()
Worksheets("Val Balance Sheet").Range("A1:D22").Copy
Dim wdapp, wddoc As Object
Dim strdocname As String
On Error Resume Next
Set wdapp = GetObject(, "Word.Application")
If Err.Number = 429 Then
Err.Clear
Set wdapp = CreateObject("Word.Application")
End If
wdapp.Visible = True
strdocname = "C:\Users\ako\report.docx"
If Dir(strdocname) = "" Then
MsgBox "The file " & strdocname & vbCrLf & "was not found " & vbCrLf & "C:\Users\ako\.", vbExclamation, "The document does not exist."
Exit Sub
End If
wdapp.Activate
Set wddoc = wdapp.documents.Open(strdocname)
If wddoc Is Nothing Then Set wddoc = wdapp.DocumentOpen(strdocnme)
wddoc.Activate
wddoc.Range.Paste
wddoc.Save
wdapp.Quit
Set wddoc = Nothing
Set wdapp = Nothing
Application.CutCopyMode = False
End Sub
Heres an update to what I was now trying, but it is only pasting the second table into word Template? What needs to be modified here to allow it to paste the multiple words without overwriting the first table.
Sub ExportToWord()
Worksheets("Val Balance Sheet").Range("A1:D22").Copy
Dim wdapp, wddoc As Object
Dim strdocname As String
On Error Resume Next
Set wdapp = GetObject(, "Word.Application")
If Err.Number = 429 Then Err.Clear
Set wdapp = CreateObject("Word.Application")
End If
wdapp.Visible = True
strdocname = "C:\Users\ako\report.docx"
If Dir(strdocname) = "" Then
MsgBox "The file " & strdocname & vbCrLf & "was not found " & vbCrLf & "C:\Users\ako\.", vbExclamation, "The document does not exist."
Exit Sub
End If
wdapp.Activate
Set wddoc = wdapp.documents.Open(strdocname)
If wddoc Is Nothing Then Set wddoc = wdapp.DocumentOpen(strdocnme)
wddoc.Activate
wddoc.Range.Paste
Worksheets("Template Gains and Losses").Range("A1:C11").Copy
Dim wdapp2, wddoc2 As Object
Dim strdocname2 As String
On Error Resume Next
Set wdapp2 = GetObject(, "Word.Application")
If Err.Number = 429 Then
Err.Clear
Set wdapp2 = CreateObject("Word.Application")
End If
wdapp2.Visible = True
strdocname2 = "C:\Users\ako\report.docx"
If Dir(strdocname2) = "" Then
MsgBox "The file " & strdocname2 & vbCrLf & "was not found " & vbCrLf & "C:\Users\ako\.", vbExclamation, "The document does not exist."
Exit Sub
End If
wdapp2.Activate
Set wddoc2 = wdapp.documents.Open(strdocname)
If wddoc2 Is Nothing Then Set wddoc2 = wdapp2.DocumentOpen(strdocnme2)
wddoc2.Activate
wddoc2.Range.Paste
wddoc.Save
wdapp.Quit
Set wddoc = Nothing
Set wdapp = Nothing
Application.CutCopyMode = False
End Sub

How to download a file from AJAX request in Liferay serveResource(-, -) method

I have a requirement like: I am making an AJAX request to pass some data to server. In my server I am creating a file using that data.
"Now problem is the file is not getting downloaded to client-side".
(I am using Apache POI API to create excel file from the given data).
Can any one will help me to do this ?
Here is my code:
(Code to make AJAX request)
<script>
function downloadUploadedBacklogs () {
try {
var table_data = [];
var count = jQuery("#backlogTable tr:first td" ).length;
jQuery("#<portlet:namespace/>noOfColumns").val(count);
var index = 0;
jQuery('tr').each(function(){
var row_data = '';
jQuery('td', this).each(function(){
row_data += jQuery(this).text() + '=';
});
table_data.push(row_data+";");
});
jQuery("#<portlet:namespace/>backlogDataForDownload").val(table_data);
jQuery("#<portlet:namespace/>cmd").val("downloadUploadedBacklogs");
alert('cmd: ' + jQuery("#<portlet:namespace/>cmd").val());
var formData = jQuery('#<portlet:namespace/>backlogImportForm').serialize();
jQuery.ajax({
url:'<%=resourceURL%>',
data:formData,
type: "post",
success: function(data) {
}
});
alert('form submitted');
} catch(e) {
alert('eroor: ' + e);
}
};
</script>
Java code serveResource(-,-) method
/*
* serveResource(-, -) method to process the client request
*/
public void serveResource(ResourceRequest resourceRequest,
ResourceResponse resourceResponse) throws IOException,
PortletException {
String cmd = ParamUtil.getString(resourceRequest,"cmd");
System.out.println("**********************cmd*************"+cmd);
if(cmd!="") {
if("downloadUploadedBacklogs".equalsIgnoreCase(cmd)){
String backlogData = ParamUtil.getString(resourceRequest, "backlogDataForDownload");
ImportBulkDataUtil.downloadUploaded("Backlogs", resourceRequest,resourceResponse);
}
}
}
/
* ImportBulkDataUtil.downloadUploaded(-, -, -) method to create excel file
/
public static void downloadUploaded(String schema, ResourceRequest resourceRequest,ResourceResponse resourceResponse) {
String excelSheetName = ParamUtil.getString(resourceRequest,"excelSheetName");
try {
resourceResponse.setContentType("application/vnd.ms-excel");
resourceResponse.addProperty(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="+excelSheetName+"_Template.xls");
OutputStream outputStream=resourceResponse.getPortletOutputStream();
//converting the POI object as excel readble object
HSSFWorkbook objHSSFWorkbook=new HSSFWorkbook();
HSSFSheet objHSSFSheet=objHSSFWorkbook.createSheet(excelSheetName+"_Template");
//set the name of the workbook
Name name=objHSSFWorkbook.createName();
name.setNameName(excelSheetName+"_Template");
objHSSFSheet.autoSizeColumn((short)2);
// create freeze pane (locking) top row
objHSSFSheet.createFreezePane(0, 1);
// Setting column width
String excelData = StringPool.BLANK;
if((schema.equalsIgnoreCase("Backlogs"))){
System.out.println("Inside BacklogsCreation..........");
objHSSFSheet.setColumnWidth(0, 10000);
objHSSFSheet.setColumnWidth(1, 7000);
objHSSFSheet.setColumnWidth(2, 7000);
objHSSFSheet.setColumnWidth(3, 7000);
objHSSFSheet.setColumnWidth(4, 7000);
objHSSFSheet.setColumnWidth(5, 5000);
objHSSFSheet.setColumnWidth(6, 5000);
objHSSFSheet.setColumnWidth(7, 7000);
objHSSFSheet.setColumnWidth(8, 7000);
excelData = ParamUtil.getString(resourceRequest,"backlogDataForDownload");
}
System.out.println("downloadUploaded excelTableData: " + excelData);
// Header creation logic
HSSFRow objHSSFRowHeader = objHSSFSheet.createRow(0);
objHSSFRowHeader.setHeightInPoints((2*objHSSFSheet.getDefaultRowHeightInPoints()));
CellStyle objHssfCellStyleHeader = objHSSFWorkbook.createCellStyle();
objHssfCellStyleHeader.setFillBackgroundColor((short)135);
objHssfCellStyleHeader.setAlignment(objHssfCellStyleHeader.ALIGN_CENTER);
objHssfCellStyleHeader.setWrapText(true);
// Apply font styles to cell styles
HSSFFont objHssfFontHeader = objHSSFWorkbook.createFont();
objHssfFontHeader.setFontName("Arial");
objHssfFontHeader.setColor(HSSFColor.WHITE.index);
HSSFColor lightGrayHeader = setColor(objHSSFWorkbook,(byte) 0x00, (byte)0x20,(byte) 0x60);
objHssfCellStyleHeader.setFillForegroundColor(lightGrayHeader.getIndex());
objHssfCellStyleHeader.setFillPattern(CellStyle.SOLID_FOREGROUND);
objHssfFontHeader.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
objHssfFontHeader.setFontHeightInPoints((short)12);
objHssfCellStyleHeader.setFont(objHssfFontHeader);
objHssfCellStyleHeader.setWrapText(true);
// first column about Backlog title
HSSFCell objBacklogTitleCell = objHSSFRowHeader.createCell(0);
objBacklogTitleCell.setCellValue("Backlog");
objBacklogTitleCell.setCellStyle(objHssfCellStyleHeader);
// second column about Description
HSSFCell objBacklogDescCell = objHSSFRowHeader.createCell(1);
objBacklogDescCell.setCellValue("Description");
objBacklogDescCell.setCellStyle(objHssfCellStyleHeader);
// third column about Project
HSSFCell objProjectNameCell = objHSSFRowHeader.createCell(2);
objProjectNameCell.setCellValue("Project");
objProjectNameCell.setCellStyle(objHssfCellStyleHeader);
setComment("Project which the backlog belongs to", objProjectNameCell);
// fourth column about Category
HSSFCell objCategoryNameCell = objHSSFRowHeader.createCell(3);
objCategoryNameCell.setCellValue("Category");
objCategoryNameCell.setCellStyle(objHssfCellStyleHeader);
setComment("Category which the backlog belongs to (i.e. Bug, New Requirement, Enhancement)", objCategoryNameCell);
// fifth column about Group
HSSFCell objGroupNameCell = objHSSFRowHeader.createCell(4);
objGroupNameCell.setCellValue("Group");
objGroupNameCell.setCellStyle(objHssfCellStyleHeader);
setComment("Group which the backlog belongs to", objGroupNameCell);
// sixth column about Est. Start Date
HSSFCell objEstStartDtCell = objHSSFRowHeader.createCell(5);
objEstStartDtCell.setCellValue("Est. Start Date");
objEstStartDtCell.setCellStyle(objHssfCellStyleHeader);
setComment("Date Format: dd/mm/yyyy", objEstStartDtCell);
// seventh column about Est. End Date
HSSFCell objEstEndDtCell = objHSSFRowHeader.createCell(6);
objEstEndDtCell.setCellValue("Est. End Date");
objEstEndDtCell.setCellStyle(objHssfCellStyleHeader);
setComment("Date Format: dd/mm/yyyy", objEstEndDtCell);
// fifth column about Group
HSSFCell objStatusCell = objHSSFRowHeader.createCell(7);
objStatusCell.setCellValue("Status");
objStatusCell.setCellStyle(objHssfCellStyleHeader);
String excelTableDataRecords[] = excelData.split(";");
for(int i=1; i<excelTableDataRecords.length; i++) {
HSSFRow objHSSFRow = objHSSFSheet.createRow(i);
objHSSFRow.setHeightInPoints((2*objHSSFSheet.getDefaultRowHeightInPoints()));
excelTableDataRecords[i] = excelTableDataRecords[i].substring(0, (excelTableDataRecords[i].length()-2));
if(excelTableDataRecords[i].charAt(0) == ',') {
excelTableDataRecords[i] = excelTableDataRecords[i].substring(1, (excelTableDataRecords[i].length()));
}
String excelTableColumns[] = excelTableDataRecords[i].split("::");
for(int j=0; j<excelTableColumns.length; j++) {
// Apply font styles to cell styles
HSSFFont objHssfFont = objHSSFWorkbook.createFont();
objHssfFont.setFontName("Arial");
CellStyle objHssfCellStyle = objHSSFWorkbook.createCellStyle();
objHssfFont.setBoldweight(HSSFFont.BOLDWEIGHT_NORMAL);
objHssfFont.setColor(HSSFColor.BLACK.index);
objHssfFont.setFontHeightInPoints((short)10);
objHssfCellStyle.setWrapText(true);
objHssfCellStyle.setFont(objHssfFont);
// other column about Backlog title
HSSFCell objNewHSSFCellFirstNameAdd = objHSSFRow.createCell(j);
objNewHSSFCellFirstNameAdd.setCellValue(excelTableColumns[j]);
objNewHSSFCellFirstNameAdd.setCellStyle(objHssfCellStyle);
}
}
objHSSFWorkbook.write(outputStream);
} catch (IOException e) {
e.printStackTrace();
System.out.println("Exception raised in downloadUploaded() method to download uploaded excel data");
}
}
Can anyone help me ?
There could be 2 issues. Either you don't send file at all or ajax is not downloading it.
From your code I can see that you writing file in response's output stream so I suspect that part is working. Maybe you can open browser developer tool to see response from server if it contains data in response body.
Second part is complicated because from nature of JS (security reason) you cannot download directly in JS itself (download will not start itself).
You need to use either iframe and append file url into and submit form to start download
$("body").append("<iframe src='" + data.message + "' style='display: none;' ></iframe>");
or
you can use new HTML5 FileAPI to do this for you in one request. Just specify blob (responseType: 'blob') type for response, convert URL from response body, append it to href attribute of newly created anchor <a> element and click on it.
See this post for more details.
Hope that helps.
You can write the contents of the POI HSSFWorkbook to a ByteArrayOutputStream and then use the toByteArray() method of the stream in Liferay's PortletResponseUtil sendFile() method as follows:
PortletResponseUtil.sendFile(resourceRequest, resourceResponse, "FILENAME", byteStream.toByteArray(), "CONTENT_TYPE");
instead of writing directly to the resourceResponse.
However, probably for security reasons, (Javascript can not directly write files to a client) you can not do this via Ajax.
You could alternatively save the raw data that you calculate in your JS code to a hidden input and pass that to the server via a regular form submit.
I think it's just your ajax command whose don't follow requirements.
See jquery ajax documentation.
It's seems ajax jquery complains xml data download but it's not accordingly with excel data format.
Set the dataType to "text" in ajax and does the good MIME type before to send the generated file to client..it's make the excel file download to be interpreted by the browser as a real excel file.
just have the request as GET, return the bytestream of file in response and set the headers accordingly (depending on the format of you file excel/pdf) and then at client side just open the response in new tab the browser would start the file download.
Just call the following function with parameters :
url - where you want to request for file data - incase you want to
send some data pageIndex - div id where u want to append iframe
and than it will be deleted without # .
this.ajaxDownload = function(url, data,pageId) {
pageId = '#' + pageId;
if ($(pageId + ' #download_iframe').length == 0) {
$("<iframe id='download_iframe' style='display: none' src='about:blank'></iframe>").appendTo(pageId);
}
var input = "<input type='hidden' name='requestJson' value='" + JSON.stringify(data) + "'>";
var iframe_html = "<html>"+
"<head>"+
"</head>"+
"<body>"+
"<form id='downloadForm' method='POST' action='" + url +"'>" +input+ "</form>" +
"</body>"+
"</html>";
var ifrm = $(pageId + ' #download_iframe')[0].contentWindow.document;
ifrm.open();
ifrm.write(iframe_html);
ifrm.close();
$(pageId + ' #download_iframe').contents().find("#downloadForm").submit();
}

d3.json Not able to post data after $ value

I am using d3.json to make request to java Controller.
While getting value in java script the data is coming correctly but while making request data after $ value is not going and truncating.Below is the code for the same.
alert(newPassword.value);
var jsonUrl="userinfoes?update=ByUserInfo&oldPassword=" + oldPassword.value + "&newPassword=" + newPasswordValue + "&ajax=false" ;
d3.json(jsonUrl,function(error,data){
alert(data);
});
While alert data is showing correctly as data$#124
but while request the data is passing as data$.
No data after $.
You have written
var jsonUrl="userinfoes?update=ByUserInfo&oldPassword=" + oldPassword.value + newPassword=" + newPassword.value + "&ajax=false" ;
Above line has syntactical errors.
try with this
var jsonUrl="userinfoes?update=ByUserInfo&oldPassword=" + oldPassword.value +"&newPassword= "+ newPassword.value + "&ajax=false" ;
Check and reply, If you still facing problem.

DWR addRows() with Element ID's

Calling All DWR Gurus!
I am currently using reverse Ajax to add data to a table in a web page dynamically.
When I run the following method:
public static void addRows(String tableBdId, String[][] data) {
Util dwrUtil = new Util(getSessionForPage()); // Get all page sessions
dwrUtil.addRows(tableBdId, data);
}
The new row gets created in my web page as required.
However, in order to update these newly created values later on the tags need to have an element ID for me to access.
I have had a look at the DWR javadoc and you can specify some additional options see http://directwebremoting.org/dwr/browser/addRows , but this makes little sense to me, the documentation is very sparse.
If anyone could give me a clue as to how I could specify the element id's for the created td elements I would be most grateful. Alternatively if anyone knows of an alternative approach I would be keen to know.
Kind Regards
Karl
The closest I could get was to pass in some arguments to give the element an id. See below:
public static void addRows(String tableBdId, String[] data, String rowId) {
Util dwrUtil = new Util(getSessionForPage()); // Get all page sessions
// Create the options, which is needed to add a row ID
String options = "{" +
" rowCreator:function(options) {" +
" var row = document.createElement(\"tr\");" +
" row.setAttribute('id','" + rowId + "'); " +
" return row;" +
" }," +
" cellCreator:function(options) {" +
" var td = document.createElement(\"td\");" +
" return td;" +
" }," +
" escapeHtml:true\"}";
// Wrap the supplied row into an array to match the API
String[][] args1 = new String[][] { data };
dwrUtil.addRows(tableBdId, args1, options);
Is this line of your code really working??
dwrUtil.addRows(tableBdId, data);
The DWR addRows method needs at least 3 parameters of 4 to work, they are:
id: The id of the table element (preferably a tbody element);
array: Array (or object from DWR 1.1) containing one entry for each row in the updated table;
cellfuncs: An array of functions (one per column) for extracting cell data from the passed row data;
options: An object containing various options.
The id, array and cellfuncs are required, and in your case, you'll have to pass the options also because you want to customize the row creation and set the TD id's. check it out:
Inside the options argument, you need to use one parameter called "cellCreator" to inform your own way to create the td html element.
Check it out:
// Use the cellFuncs var to set the values you want to display inside the table rows
// the syntax is object.property
// use one function(data) for each property you need to show on your table.
var cellFuncs = [
function(data) { return data.name_of_the_first_object_property ,
function(data) { return data.name_of_the_second_object_property; }
];
DWRUtil.addRows(
tableBdId,
data,
cellFuncs,
{
// This function is used for you customize the generated td element
cellCreator:function(options) {
var td = document.createElement("td");
// setting the td element id using the rowIndex
// just implement your own id bellow
td.id = options.rowIndex;
return td;
}
});

Categories

Resources