Initial commit

This commit is contained in:
2025-08-04 16:33:07 +03:30
commit f798e8e35c
9595 changed files with 1208683 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
import { ref } from 'vue'
export function useDataTableActions(gridApi, rowData) {
const showDetailsDialog = ref(false)
const showDeleteDialog = ref(false)
const selectedRowData = ref(null)
const deleteRowData = ref(null)
const getRowDataByIndex = (rowIndex) => {
if (!gridApi.value) {
console.error('Grid API not available')
return null
}
try {
const node = gridApi.value.getDisplayedRowAtIndex(rowIndex)
if (node && node.data) {
return node.data
}
if (rowData.value && rowData.value[rowIndex]) {
return rowData.value[rowIndex]
}
console.error('Row data not found for index:', rowIndex)
return null
} catch (error) {
console.error('Error getting row data:', error)
return null
}
}
const showDetails = (rowIndex, nodeId) => {
console.log('Show details called with:', { rowIndex, nodeId })
const rowData = getRowDataByIndex(rowIndex)
if (rowData) {
selectedRowData.value = { ...rowData }
showDetailsDialog.value = true
console.log('Selected row data:', selectedRowData.value)
} else {
console.error('Failed to get row data for details')
}
}
const editRow = (rowIndex, nodeId) => {
console.log('Edit row called with:', { rowIndex, nodeId })
if (!gridApi.value) {
console.error('Grid API not available for editing')
return
}
try {
gridApi.value.startEditingCell({
rowIndex: rowIndex,
colKey: 'fullName'
})
console.log('Started inline editing for row:', rowIndex)
} catch (error) {
console.error('Error starting inline edit:', error)
}
}
const deleteRow = (rowIndex, nodeId) => {
console.log('Delete row called with:', { rowIndex, nodeId })
const rowData = getRowDataByIndex(rowIndex)
if (rowData) {
deleteRowData.value = { ...rowData }
showDeleteDialog.value = true
console.log('Delete row data:', deleteRowData.value)
} else {
console.error('Failed to get row data for delete')
}
}
const confirmDelete = () => {
if (deleteRowData.value && gridApi.value) {
try {
const indexToDelete = rowData.value.findIndex(item => {
return (
(item.id && item.id === deleteRowData.value.id) ||
(item.email && item.email === deleteRowData.value.email) ||
(item.fullName === deleteRowData.value.fullName &&
item.salary === deleteRowData.value.salary)
)
})
if (indexToDelete !== -1) {
rowData.value.splice(indexToDelete, 1)
gridApi.value.applyTransaction({
remove: [deleteRowData.value]
})
console.log('Row deleted successfully')
} else {
console.error('Row not found for deletion')
}
} catch (error) {
console.error('Error deleting row:', error)
}
}
showDeleteDialog.value = false
deleteRowData.value = null
}
const cancelDelete = () => {
showDeleteDialog.value = false
deleteRowData.value = null
}
const saveUser = (editedData) => {
if (!editedData || !gridApi.value) return
try {
const indexToUpdate = rowData.value.findIndex(item => {
return (
(item.id && item.id === editedData.id) ||
(item.email && item.email === selectedRowData.value.email) ||
(item.fullName === selectedRowData.value.fullName)
)
})
if (indexToUpdate !== -1) {
rowData.value[indexToUpdate] = { ...editedData }
const node = gridApi.value.getRowNode(indexToUpdate)
if (node) {
node.setData(editedData)
}
console.log('User updated successfully:', editedData)
} else {
console.error('User not found for update')
}
} catch (error) {
console.error('Error updating user:', error)
}
showDetailsDialog.value = false
selectedRowData.value = null
}
const exportToCSV = () => {
if (gridApi.value) {
gridApi.value.exportDataAsCsv({
fileName: `datatable-export-${new Date().toISOString().split('T')[0]}.csv`,
columnSeparator: ',',
suppressQuotes: false,
onlySelected: false,
skipGroups: true,
skipHeader: false,
skipFooters: true,
skipPinnedTop: true,
skipPinnedBottom: true,
allColumns: false,
onlySelectedAllPages: false
})
}
}
const exportToExcel = () => {
if (gridApi.value) {
gridApi.value.exportDataAsExcel({
fileName: `datatable-export-${new Date().toISOString().split('T')[0]}.xlsx`,
sheetName: 'Data Export',
onlySelected: false,
skipGroups: true,
skipHeader: false,
skipFooters: true,
skipPinnedTop: true,
skipPinnedBottom: true,
allColumns: false,
onlySelectedAllPages: false
})
}
}
const setupGlobalHandlers = () => {
window.handleShowDetails = showDetails
window.handleEditRow = editRow
window.handleDeleteRow = deleteRow
}
const cleanupGlobalHandlers = () => {
delete window.handleShowDetails
delete window.handleEditRow
delete window.handleDeleteRow
}
return {
showDetailsDialog,
showDeleteDialog,
selectedRowData,
deleteRowData,
showDetails,
editRow,
deleteRow,
confirmDelete,
cancelDelete,
saveUser,
exportToCSV,
exportToExcel,
setupGlobalHandlers,
cleanupGlobalHandlers
}
}

View File

@@ -0,0 +1,614 @@
import { ref, computed } from "vue";
export function useDataTableGrid(data, isMobile, isTablet) {
let gridApi = ref(null);
const rowData = ref([]);
const statusCellRenderer = (params) => {
const s = params.value;
let label = "Applied";
let colorClass = "info";
if (s === 1) {
label = "Current";
colorClass = "primary";
}
if (s === 2) {
label = "Professional";
colorClass = "success";
}
if (s === 3) {
label = "Rejected";
colorClass = "error";
}
if (s === 4) {
label = "Resigned";
colorClass = "warning";
}
const chipSize = isMobile.value ? "x-small" : "small";
const fontSize = isMobile.value ? "10px" : "12px";
return `<div class="v-chip v-chip--size-${chipSize} v-chip--variant-flat bg-${colorClass}"
style="
height:${isMobile.value ? "20px" : "24px"};
padding:0 ${isMobile.value ? "6px" : "8px"};
display:inline-flex;
align-items:center;
border-radius:12px;
font-size:${fontSize};
font-weight:500;
color:white;
border: inherit;
">${label}</div>`;
};
const statusCellEditor = () => {
const statusOptions = [
{ value: 0, label: "Applied" },
{ value: 1, label: "Current" },
{ value: 2, label: "Professional" },
{ value: 3, label: "Rejected" },
{ value: 4, label: "Resigned" },
];
let currentValue = 0;
let selectElement = null;
const editorObject = {
init: (params) => {
currentValue = params.value || 0;
selectElement = document.createElement("select");
selectElement.style.cssText =
"width: 100%; height: 100%; border: none; outline: none; font-size: 12px; background: white; color: black; padding: 4px;";
statusOptions.forEach((option) => {
const optionElement = document.createElement("option");
optionElement.value = option.value;
optionElement.text = option.label;
if (option.value == currentValue) {
optionElement.selected = true;
}
selectElement.appendChild(optionElement);
});
selectElement.addEventListener("change", (e) => {
currentValue = parseInt(e.target.value);
});
selectElement.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
currentValue = parseInt(selectElement.value);
params.stopEditing();
}
if (e.key === "Escape") {
params.stopEditing(true);
}
});
},
getGui: () => selectElement,
getValue: () => parseInt(currentValue),
afterGuiAttached: () => {
selectElement.focus();
selectElement.click();
},
isPopup: () => false,
};
return editorObject;
};
const actionButtonsRenderer = (params) => {
const iconSize = isMobile.value ? "14px" : "16px";
const buttonSize = isMobile.value ? "24px" : "28px";
const gap = isMobile.value ? "3px" : "4px";
const rowIndex = params.node.rowIndex;
const nodeId = params.node.id;
return `
<div style="display: flex; gap: ${gap}; align-items: center; justify-content: center; padding: 2px 0;">
<button
class="action-btn details-btn"
style="
background: rgba(var(--v-theme-primary), 0.15);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(var(--v-theme-primary), 0.3);
color: rgb(var(--v-theme-primary));
border-radius: 12px;
width: ${buttonSize};
height: ${buttonSize};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 8px 32px rgba(var(--v-theme-primary), 0.12);
position: relative;
overflow: hidden;
"
onmouseover="
this.style.transform='translateY(-2px) scale(1.05)';
this.style.background='rgba(var(--v-theme-primary), 0.25)';
this.style.borderColor='rgba(var(--v-theme-primary), 0.5)';
this.style.boxShadow='0 12px 40px rgba(var(--v-theme-primary), 0.2)';
"
onmouseout="
this.style.transform='translateY(0) scale(1)';
this.style.background='rgba(var(--v-theme-primary), 0.15)';
this.style.borderColor='rgba(var(--v-theme-primary), 0.3)';
this.style.boxShadow='0 8px 32px rgba(var(--v-theme-primary), 0.12)';
"
onclick="handleShowDetails(${rowIndex}, '${nodeId}')"
title="View Details"
>
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
<button
class="action-btn edit-btn"
style="
background: rgba(var(--v-theme-warning), 0.15);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(var(--v-theme-warning), 0.3);
color: rgb(var(--v-theme-warning));
border-radius: 12px;
width: ${buttonSize};
height: ${buttonSize};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 8px 32px rgba(var(--v-theme-warning), 0.12);
position: relative;
overflow: hidden;
"
onmouseover="
this.style.transform='translateY(-2px) scale(1.05)';
this.style.background='rgba(var(--v-theme-warning), 0.25)';
this.style.borderColor='rgba(var(--v-theme-warning), 0.5)';
this.style.boxShadow='0 12px 40px rgba(var(--v-theme-warning), 0.2)';
"
onmouseout="
this.style.transform='translateY(0) scale(1)';
this.style.background='rgba(var(--v-theme-warning), 0.15)';
this.style.borderColor='rgba(var(--v-theme-warning), 0.3)';
this.style.boxShadow='0 8px 32px rgba(var(--v-theme-warning), 0.12)';
"
onclick="handleEditRow(${rowIndex}, '${nodeId}')"
title="Edit Row"
>
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button
class="action-btn delete-btn"
style="
background: rgba(var(--v-theme-error), 0.15);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(var(--v-theme-error), 0.3);
color: rgb(var(--v-theme-error));
border-radius: 12px;
width: ${buttonSize};
height: ${buttonSize};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 8px 32px rgba(var(--v-theme-error), 0.12);
position: relative;
overflow: hidden;
"
onmouseover="
this.style.transform='translateY(-2px) scale(1.05)';
this.style.background='rgba(var(--v-theme-error), 0.25)';
this.style.borderColor='rgba(var(--v-theme-error), 0.5)';
this.style.boxShadow='0 12px 40px rgba(var(--v-theme-error), 0.2)';
"
onmouseout="
this.style.transform='translateY(0) scale(1)';
this.style.background='rgba(var(--v-theme-error), 0.15)';
this.style.borderColor='rgba(var(--v-theme-error), 0.3)';
this.style.boxShadow='0 8px 32px rgba(var(--v-theme-error), 0.12)';
"
onclick="handleDeleteRow(${rowIndex}, '${nodeId}')"
title="Delete Row"
>
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6m3 0V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</button>
</div>
`;
};
const columnDefs = ref([
{
headerName: "NAME",
field: "fullName",
sortable: true,
filter: true,
flex: 1,
minWidth: 150,
hide: false,
editable: true,
cellEditor: "agTextCellEditor",
cellEditorParams: {
maxLength: 50,
},
onCellValueChanged: (params) => {
console.log("Name changed:", params.newValue);
},
},
{
headerName: "EMAIL",
field: "email",
sortable: true,
filter: true,
flex: 1.5,
minWidth: 200,
hide: false,
editable: true,
cellEditor: "agTextCellEditor",
cellEditorParams: {
maxLength: 100,
},
onCellValueChanged: (params) => {
console.log("Email changed:", params.newValue);
},
},
{
headerName: "DATE",
field: "startDate",
sortable: true,
filter: true,
flex: 1,
minWidth: 120,
hide: false,
editable: true,
cellEditor: "agDateCellEditor",
cellEditorParams: {
preventEdit: (params) => {
return false;
},
},
valueFormatter: (params) => {
if (!params.value) return "";
const date = new Date(params.value);
if (isNaN(date.getTime())) return "";
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
return `${day}/${month}/${year}`;
},
valueParser: (params) => {
if (!params.newValue) return null;
const dateStr = params.newValue;
if (typeof dateStr === "string" && dateStr.includes("/")) {
const parts = dateStr.split("/");
if (parts.length === 3) {
const day = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const year = parseInt(parts[2]);
const date = new Date(year, month, day);
if (!isNaN(date.getTime())) {
return date;
}
}
}
return new Date(dateStr);
},
filterParams: {
buttons: ["reset"],
closeOnApply: false,
suppressAndOrCondition: true,
comparator: (filterLocalDateAtMidnight, cellValue) => {
if (!cellValue) return -1;
const cellDate = new Date(cellValue);
cellDate.setHours(0, 0, 0, 0);
if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) {
return 0;
}
return filterLocalDateAtMidnight < cellDate ? -1 : 1;
},
},
onCellValueChanged: (params) => {
if (
params.newValue === "" ||
params.newValue === null ||
params.newValue === undefined
) {
params.node.setDataValue(params.colDef.field, params.oldValue);
}
console.log("Date changed:", params.newValue);
},
},
{
headerName: "SALARY",
field: "salary",
sortable: true,
filter: true,
flex: 1,
minWidth: 120,
hide: false,
editable: true,
cellEditor: "agNumberCellEditor",
cellEditorParams: {
precision: 0,
},
filterParams: {
buttons: ["reset"],
closeOnApply: false,
suppressAndOrCondition: true,
inRangeInclusive: true,
debounceMs: 200,
},
valueFormatter: (params) => {
if (params.value) {
const intValue = Math.floor(Number(params.value));
return `$${intValue.toLocaleString()}`;
}
return "";
},
onCellValueChanged: (params) => {
if (
params.newValue !== null &&
params.newValue !== undefined &&
params.newValue !== ""
) {
const numValue = parseInt(params.newValue);
if (!isNaN(numValue)) {
params.node.setDataValue(params.colDef.field, numValue);
} else {
params.node.setDataValue(params.colDef.field, params.oldValue);
}
} else {
params.node.setDataValue(params.colDef.field, params.oldValue);
}
console.log("Salary changed:", params.newValue);
},
},
{
headerName: "AGE",
field: "age",
sortable: true,
filter: true,
flex: 0.7,
minWidth: 80,
hide: false,
editable: true,
cellEditor: "agNumberCellEditor",
cellEditorParams: {
precision: 0,
showStepperButtons: false,
},
valueFormatter: (params) => {
if (
params.value === null ||
params.value === undefined ||
params.value === ""
) {
return "";
}
const numValue = Number(params.value);
return isNaN(numValue) ? "" : Math.floor(numValue).toString();
},
onCellValueChanged: (params) => {
const newValue = params.newValue;
if (newValue === null || newValue === undefined || newValue === "") {
params.node.setDataValue(params.colDef.field, params.oldValue);
return;
}
const cleanValue = String(newValue).trim();
if (cleanValue === "") {
params.node.setDataValue(params.colDef.field, params.oldValue);
return;
}
const parsed = parseInt(cleanValue, 10);
if (isNaN(parsed)) {
console.warn("Age: Invalid number format:", cleanValue);
params.node.setDataValue(params.colDef.field, params.oldValue);
return;
}
if (parsed < 1 || parsed > 120) {
console.warn("Age: Out of range (1-120):", parsed);
params.node.setDataValue(params.colDef.field, params.oldValue);
return;
}
params.node.setDataValue(params.colDef.field, parsed);
console.log("Age changed from", params.oldValue, "to", parsed);
},
},
{
headerName: "STATUS",
field: "status",
sortable: true,
filter: true,
flex: 1,
minWidth: 100,
hide: false,
editable: true,
cellRenderer: statusCellRenderer,
cellEditor: statusCellEditor,
onCellValueChanged: (params) => {
console.log("Status changed:", params.newValue);
},
},
{
headerName: "ACTION",
field: "action",
sortable: false,
filter: false,
flex: 1,
minWidth: 100,
hide: false,
editable: false,
cellRenderer: actionButtonsRenderer,
suppressMenu: true,
suppressSorting: true,
suppressFilter: true,
},
]);
const defaultColDef = computed(() => ({
resizable: true,
sortable: true,
filter: true,
floatingFilter: false,
cellStyle: { display: "flex", alignItems: "center" },
flex: 1,
minWidth: isMobile.value ? 80 : 100,
maxWidth: 400,
wrapText: false,
autoHeight: false,
suppressMenu: false,
}));
const gridOptions = computed(() => ({
theme: "legacy",
headerHeight: isMobile.value ? 48 : 56,
rowHeight: isMobile.value ? 44 : 52,
animateRows: true,
rowSelection: { type: "multiRow" },
pagination: true,
paginationPageSize: isMobile.value ? 5 : 10,
paginationPageSizeSelector: isMobile.value ? [5, 10, 20] : [5, 10, 20, 50],
suppressRowClickSelection: false,
rowMultiSelectWithClick: true,
enableCellTextSelection: true,
suppressHorizontalScroll: false,
alwaysShowHorizontalScroll: false,
suppressColumnVirtualisation: false,
autoGroupColumnDef: {
minWidth: 200,
maxWidth: 400,
flex: 1,
},
editType: "fullRow",
stopEditingWhenCellsLoseFocus: true,
enterNavigatesVertically: true,
enterNavigatesVerticallyAfterEdit: true,
singleClickEdit: false,
suppressClickEdit: false,
includeHiddenColumnsInQuickFilter: false,
cacheQuickFilter: true,
enableAdvancedFilter: false,
includeHiddenColumnsInAdvancedFilter: false,
suppressBrowserResizeObserver: false,
maintainColumnOrder: true,
suppressMenuHide: true,
enableRangeSelection: true,
enableFillHandle: false,
enableRangeHandle: false,
}));
const updateColumnVisibility = () => {
if (!gridApi.value) return;
if (isMobile.value) {
gridApi.value.setColumnVisible("email", false);
gridApi.value.setColumnVisible("startDate", false);
gridApi.value.setColumnVisible("age", false);
} else if (isTablet.value) {
gridApi.value.setColumnVisible("email", true);
gridApi.value.setColumnVisible("startDate", false);
gridApi.value.setColumnVisible("age", true);
} else {
gridApi.value.setColumnVisible("email", true);
gridApi.value.setColumnVisible("startDate", true);
gridApi.value.setColumnVisible("age", true);
}
};
const updatePagination = () => {
if (!gridApi.value) return;
const pageSize = isMobile.value ? 5 : 10;
const pageSizeSelector = isMobile.value ? [5, 10, 20] : [5, 10, 20, 50];
gridApi.value.paginationSetPageSize(pageSize);
gridApi.value.setGridOption("paginationPageSizeSelector", pageSizeSelector);
};
function onGridReady(params) {
gridApi.value = params.api;
rowData.value = data.map((item) => ({
...item,
salary:
typeof item.salary === "string" ? parseInt(item.salary) : item.salary,
age: typeof item.age === "string" ? parseInt(item.age) : item.age,
}));
updateColumnVisibility();
updatePagination();
setTimeout(() => {
gridApi.value.sizeColumnsToFit();
}, 100);
}
function onCellValueChanged(params) {
console.log("Cell value changed:", {
field: params.colDef.field,
oldValue: params.oldValue,
newValue: params.newValue,
data: params.data,
});
const dataIndex = rowData.value.findIndex((item) => {
return (
(item.id && item.id === params.data.id) ||
(item.email && item.email === params.data.email) ||
item.fullName === params.data.fullName
);
});
if (dataIndex !== -1) {
rowData.value[dataIndex] = { ...params.data };
}
}
return {
gridApi,
columnDefs,
rowData,
defaultColDef,
gridOptions,
onGridReady,
onCellValueChanged,
updateColumnVisibility,
updatePagination,
};
}

View File

@@ -0,0 +1,27 @@
import { ref, onMounted, onUnmounted } from 'vue'
export function useResponsive() {
const windowWidth = ref(window.innerWidth)
const isMobile = ref(window.innerWidth < 768)
const isTablet = ref(window.innerWidth >= 768 && window.innerWidth < 1024)
const handleResize = () => {
windowWidth.value = window.innerWidth
isMobile.value = window.innerWidth < 768
isTablet.value = window.innerWidth >= 768 && window.innerWidth < 1024
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
return {
windowWidth,
isMobile,
isTablet
}
}