Added a TodoChart component, adding a reactive Todo/Completed items tracker.
This commit is contained in:
parent
d4545d753a
commit
d154c28ed2
2 changed files with 552 additions and 157 deletions
280
frontend/src/components/common/TodoChart.vue
Normal file
280
frontend/src/components/common/TodoChart.vue
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
<template>
|
||||||
|
<!--<div class="todo-chart-container"> -->
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<!--<div v-if="loading" class="loading-overlay">-->
|
||||||
|
<!-- <div class="spinner"></div>-->
|
||||||
|
<!-- <div class="loading-text">Loading chart data...</div>-->
|
||||||
|
<!--</div>-->
|
||||||
|
<!-- Chart Container -->
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas ref="chartCanvas" class="chart-canvas" v-show="!loading"></canvas>
|
||||||
|
<!-- Center Data Display -->
|
||||||
|
<div class="center-data" v-if="centerData && !loading">
|
||||||
|
<div class="center-label">{{ centerData.label }}</div>
|
||||||
|
<div class="center-value">{{ centerData.value }}</div>
|
||||||
|
<div class="center-percentage" v-if="centerData.percentage">
|
||||||
|
{{ centerData.percentage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--</div> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, nextTick, computed, onUnmounted} from "vue";
|
||||||
|
import { Chart, registerables } from "chart.js";
|
||||||
|
|
||||||
|
// Register Chart.js components
|
||||||
|
Chart.register(...registerables);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: String,
|
||||||
|
todoNumber: Number,
|
||||||
|
completedNumber: Number,
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//Constants
|
||||||
|
const categories = ["To-do", "Completed"];
|
||||||
|
|
||||||
|
//Reactive data
|
||||||
|
const centerData = ref(null);
|
||||||
|
const hoveredSegment = ref(null);
|
||||||
|
const chartCanvas = ref(null);
|
||||||
|
const chartInstance = ref(null);
|
||||||
|
|
||||||
|
// Handle view changes
|
||||||
|
const handleViewChange = () => {
|
||||||
|
updateChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHoveredCategoryIndex = () => {
|
||||||
|
return hoveredSegment.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryValue = (categoryIndex) => {
|
||||||
|
if (categoryIndex === 0) {
|
||||||
|
return props.todoNumber
|
||||||
|
} else {
|
||||||
|
return props.completedNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChartData = () => {
|
||||||
|
const chartData = {
|
||||||
|
name: props.title,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "",
|
||||||
|
data: [props.todoNumber, props.completedNumber],
|
||||||
|
backgroundColor: ["#b22222", "#4caf50"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
return chartData;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const updateCenterData = () => {
|
||||||
|
const total = props.todoNumber + props.completedNumber;
|
||||||
|
const todos = props.todoNumber;
|
||||||
|
|
||||||
|
if (todos === 0 && total > 0) {
|
||||||
|
centerData.value = {
|
||||||
|
label: "Completed",
|
||||||
|
value: "0",
|
||||||
|
percentage: "100%",
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (todos === 0 || isNaN(todos)){
|
||||||
|
centerData.value = {
|
||||||
|
label: "No To-Dos",
|
||||||
|
value: "0",
|
||||||
|
percentage: null,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoveredCategoryIndex = getHoveredCategoryIndex()
|
||||||
|
if (hoveredCategoryIndex !== null) {
|
||||||
|
// Show specific segment data when hovered
|
||||||
|
const value = getCategoryValue(hoveredCategoryIndex);
|
||||||
|
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) + "%" : "0%";
|
||||||
|
|
||||||
|
centerData.value = {
|
||||||
|
label: categories[hoveredCategoryIndex],
|
||||||
|
value: value,
|
||||||
|
percentage: percentage,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
centerData.value = {
|
||||||
|
label: "To-do",
|
||||||
|
value: props.todoNumber,
|
||||||
|
percentage: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chart options
|
||||||
|
const getChartOptions = () => {
|
||||||
|
return {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
cutout: "60%",
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: "bottom",
|
||||||
|
labels: {
|
||||||
|
padding: 20,
|
||||||
|
usePointStyle: true,
|
||||||
|
font: { size: 12 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: { enabled: false },
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: props.title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
arc: {
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
animateRotate: true,
|
||||||
|
animateScale: true,
|
||||||
|
duration: 1000,
|
||||||
|
easing: "easeOutQuart",
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: "nearest",
|
||||||
|
intersect: true,
|
||||||
|
},
|
||||||
|
onHover: (event, elements) => {
|
||||||
|
const categoryIndex = getHoveredCategoryIndex();
|
||||||
|
const total = getCategoryValue(categoryIndex);
|
||||||
|
|
||||||
|
if (elements && elements.length > 0) {
|
||||||
|
const elementIndex = elements[0].index;
|
||||||
|
if (hoveredSegment.value !== elementIndex) {
|
||||||
|
hoveredSegment.value = elementIndex;
|
||||||
|
updateCenterData();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hoveredSegment.value !== null) {
|
||||||
|
hoveredSegment.value = null;
|
||||||
|
updateCenterData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createChart = () => {
|
||||||
|
if (!chartCanvas.value || props.loading) return;
|
||||||
|
|
||||||
|
console.log(`DEBUG: Creating chart for ${props.title}`);
|
||||||
|
console.log(props);
|
||||||
|
|
||||||
|
const ctx = chartCanvas.value.getContext("2d");
|
||||||
|
if (chartInstance.value) {
|
||||||
|
chartInstance.value.destroy();
|
||||||
|
}
|
||||||
|
const chart = new Chart(ctx, {
|
||||||
|
type: "doughnut",
|
||||||
|
data: getChartData(),
|
||||||
|
options: getChartOptions(),
|
||||||
|
});
|
||||||
|
// Don't let Vue mutate Chart members for reactivity
|
||||||
|
Object.seal(chart);
|
||||||
|
chartInstance.value = chart;
|
||||||
|
// Populate Chart display
|
||||||
|
updateCenterData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update chart
|
||||||
|
const updateChart = () => {
|
||||||
|
if (props.loading || !chartInstance.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newData = getChartData();
|
||||||
|
chartInstance.value.data = newData;
|
||||||
|
chartInstance.value.update("none");
|
||||||
|
updateCenterData();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
createChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.completedNumber, (newValue) => {
|
||||||
|
updateChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/*.todo-chart-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
min-height: 400px;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas {
|
||||||
|
max-height: 100%;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-data {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-percentage {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,203 +1,300 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<h1 class="dashboard-title">Dashboard</h1>
|
<h1 class="dashboard-title">Today's Tasks</h1>
|
||||||
|
|
||||||
<div class="widgets-grid">
|
<div class="widgets-grid">
|
||||||
<!-- Calendar Widget -->
|
<!-- Locates Widget -->
|
||||||
<Card class="widget-card">
|
<Card class="widget-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="widget-header">
|
<div class="widget-header">
|
||||||
<Calendar class="widget-icon" />
|
<ShieldSearch class="widget-icon" />
|
||||||
<h3>Service Calendar</h3>
|
<h3>Locates</h3>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="widget-content">
|
<div class="widget-content">
|
||||||
<div class="metric">
|
<TodoChart
|
||||||
<span class="metric-number">8</span>
|
title="Locates"
|
||||||
<span class="metric-label">Services Scheduled Today</span>
|
:todoNumber="locatesTodoNumber"
|
||||||
</div>
|
:completedNumber="locatesCompletedNumber"
|
||||||
<div class="metric">
|
>
|
||||||
<span class="metric-number">15</span>
|
</TodoChart>
|
||||||
<span class="metric-label">Services This Week</span>
|
<button class="sidebar-button" @click="navigateTo('/jobs')">
|
||||||
</div>
|
View Locates
|
||||||
<Button
|
</button>
|
||||||
label="View Calendar"
|
|
||||||
size="small"
|
|
||||||
outlined
|
|
||||||
@click="navigateTo('/calendar')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
<!-- Permits Widget -->
|
||||||
<!-- Clients Widget -->
|
|
||||||
<Card class="widget-card">
|
<Card class="widget-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="widget-header">
|
<div class="widget-header">
|
||||||
<Community class="widget-icon" />
|
<ClipboardCheck class="widget-icon" />
|
||||||
<h3>Client Contact List</h3>
|
<h3>Permits</h3>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="widget-content">
|
<div class="widget-content">
|
||||||
<div class="status-row">
|
<TodoChart
|
||||||
<Tag severity="success" value="5 Active Jobs" />
|
title="Permits"
|
||||||
<Tag severity="warning" value="3 Pending Estimates" />
|
:todoNumber="permitsTodoNumber"
|
||||||
</div>
|
:completedNumber="permitsCompletedNumber"
|
||||||
<div class="metric">
|
>
|
||||||
<span class="metric-number">{{ clientData.length }}</span>
|
</TodoChart>
|
||||||
<span class="metric-label">Total Client Records</span>
|
<button class="sidebar-button" @click="navigateTo('/jobs')">
|
||||||
</div>
|
View Permits
|
||||||
<Button
|
</button>
|
||||||
label="View Client List"
|
|
||||||
size="small"
|
|
||||||
outlined
|
|
||||||
@click="navigateTo('/clients')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
<!-- Permits Finalization Widget -->
|
||||||
<!-- Jobs Widget -->
|
|
||||||
<Card class="widget-card">
|
<Card class="widget-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="widget-header">
|
<div class="widget-header">
|
||||||
<Hammer class="widget-icon" />
|
<DoubleCheck class="widget-icon" />
|
||||||
<h3>Job Management</h3>
|
<h3>Permit Finalizations</h3>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="widget-content">
|
<div class="widget-content">
|
||||||
<div class="status-row">
|
<TodoChart
|
||||||
<Tag severity="info" value="4 In Progress" />
|
title="Permit Finalization"
|
||||||
<Tag severity="success" value="2 Completed" />
|
:todoNumber="permitFinalizationsTodoNumber"
|
||||||
</div>
|
:completedNumber="permitFinalizationsCompletedNumber"
|
||||||
<div class="metric">
|
>
|
||||||
<span class="metric-number">{{
|
</TodoChart>
|
||||||
jobData.filter((job) => job.overAllStatus === "in progress").length
|
<button class="sidebar-button" @click="navigateTo('/jobs')">
|
||||||
}}</span>
|
View Finalizations
|
||||||
<span class="metric-label">Jobs In Progress</span>
|
</button>
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
label="View All Jobs"
|
|
||||||
size="small"
|
|
||||||
outlined
|
|
||||||
@click="navigateTo('/jobs')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Routes Widget -->
|
|
||||||
<Card class="widget-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="widget-header">
|
|
||||||
<PathArrowSolid class="widget-icon" />
|
|
||||||
<h3>Service Routes</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<div class="widget-content">
|
|
||||||
<div class="metric">
|
|
||||||
<span class="metric-number">6</span>
|
|
||||||
<span class="metric-label">Active Routes Today</span>
|
|
||||||
</div>
|
|
||||||
<div class="metric">
|
|
||||||
<span class="metric-number">45.2</span>
|
|
||||||
<span class="metric-label">Avg. Miles per Route</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
label="View Routes"
|
|
||||||
size="small"
|
|
||||||
outlined
|
|
||||||
@click="navigateTo('/routes')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Time Sheets Widget -->
|
|
||||||
<Card class="widget-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="widget-header">
|
|
||||||
<Clock class="widget-icon" />
|
|
||||||
<h3>Employee Timesheets</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<div class="widget-content">
|
|
||||||
<div class="metric">
|
|
||||||
<span class="metric-number">32.5</span>
|
|
||||||
<span class="metric-label">Total Hours This Week</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-row">
|
|
||||||
<Tag severity="success" value="5 Approved" />
|
|
||||||
<Tag severity="warning" value="2 Pending Approval" />
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
label="View Timesheets"
|
|
||||||
size="small"
|
|
||||||
outlined
|
|
||||||
@click="navigateTo('/timesheets')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Warranties Widget -->
|
<!-- Warranties Widget -->
|
||||||
<Card class="widget-card">
|
<Card class="widget-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="widget-header">
|
<div class="widget-header">
|
||||||
<HistoricShield class="widget-icon" />
|
<Shield class="widget-icon" />
|
||||||
<h3>Warranty Claims</h3>
|
<h3>Warranties</h3>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="widget-content">
|
<div class="widget-content">
|
||||||
<div class="metric">
|
<TodoChart
|
||||||
<span class="metric-number">10</span>
|
title="Warranty Claims"
|
||||||
<span class="metric-label">Open Claims</span>
|
:todoNumber="warrantyTodoNumber"
|
||||||
</div>
|
:completedNumber="warrantyCompletedNumber"
|
||||||
<div class="status-row">
|
>
|
||||||
<Tag severity="success" value="3 Completed" />
|
</TodoChart>
|
||||||
<Tag severity="warning" value="4 In Progress" />
|
<button class="sidebar-button" @click="navigateTo('/jobs')">
|
||||||
</div>
|
View Warranties
|
||||||
<Button
|
</button>
|
||||||
label="View Claims"
|
|
||||||
size="small"
|
|
||||||
outlined
|
|
||||||
@click="navigateTo('/warranties')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
<!-- Incomplete Bids Widget -->
|
||||||
|
<Card class="widget-card">
|
||||||
<!-- Quick Stats Summary -->
|
|
||||||
<div class="summary-section">
|
|
||||||
<Card>
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<h3>Quick Stats</h3>
|
<div class="widget-header">
|
||||||
|
<Edit class="widget-icon" />
|
||||||
|
<h3>Incomplete Bids</h3>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="stats-grid">
|
<div class="widget-content">
|
||||||
<div class="stat-item">
|
<TodoChart
|
||||||
<span class="stat-value">{{ totalRevenue }}</span>
|
title="Incomplete Bids"
|
||||||
<span class="stat-label">Monthly Revenue</span>
|
:todoNumber="bidsTodoNumber"
|
||||||
|
:completedNumber="bidsCompletedNumber"
|
||||||
|
>
|
||||||
|
</TodoChart>
|
||||||
|
<button class="sidebar-button"
|
||||||
|
@click="navigateTo('/calendar')">
|
||||||
|
Incomplete Bids
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
</template>
|
||||||
<span class="stat-value">{{ completedJobs }}</span>
|
</Card>
|
||||||
<span class="stat-label">Jobs Completed</span>
|
<!-- Unapproved Estimates Widget -->
|
||||||
|
<Card class="widget-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="widget-header">
|
||||||
|
<ChatBubbleQuestion class="widget-icon" />
|
||||||
|
<h3>Unapproved Estimates</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
</template>
|
||||||
<span class="stat-value">{{ clientSatisfaction }}%</span>
|
<template #content>
|
||||||
<span class="stat-label">Client Satisfaction</span>
|
<div class="widget-content">
|
||||||
|
<TodoChart
|
||||||
|
title="Unapproved Estimates"
|
||||||
|
:todoNumber="estimatesTodoNumber"
|
||||||
|
:completedNumber="estimatesCompletedNumber"
|
||||||
|
>
|
||||||
|
</TodoChart>
|
||||||
|
<button class="sidebar-button"
|
||||||
|
@click="navigateTo('/estimates')">
|
||||||
|
Unapproved Estimates
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
</template>
|
||||||
<span class="stat-value">{{ avgResponseTime }}h</span>
|
</Card>
|
||||||
<span class="stat-label">Avg Response Time</span>
|
<!-- Half Down Widget -->
|
||||||
|
<Card class="widget-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="widget-header">
|
||||||
|
<CreditCard class="widget-icon" />
|
||||||
|
<h3>Half Down Payments</h3>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="widget-content">
|
||||||
|
<TodoChart
|
||||||
|
title="Half Down Payments"
|
||||||
|
:todoNumber="halfDownTodoNumber"
|
||||||
|
:completedNumber="halfDownCompletedNumber"
|
||||||
|
>
|
||||||
|
</TodoChart>
|
||||||
|
<button class="sidebar-button"
|
||||||
|
@click="navigateTo('/jobs')">
|
||||||
|
Half Down Payments
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
<!-- Late Balances Widget -->
|
||||||
|
<Card class="widget-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="widget-header">
|
||||||
|
<CardNoAccess class="widget-icon" />
|
||||||
|
<h3>Late Balances</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="widget-content">
|
||||||
|
<TodoChart
|
||||||
|
title="Late Balances"
|
||||||
|
:todoNumber="balancesTodoNumber"
|
||||||
|
:completedNumber="balancesCompletedNumber"
|
||||||
|
>
|
||||||
|
</TodoChart>
|
||||||
|
<button class="sidebar-button"
|
||||||
|
@click="navigateTo('/jobs')">
|
||||||
|
Late Balances
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
<!-- Backflow Tests Widget -->
|
||||||
|
<Card class="widget-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="widget-header">
|
||||||
|
<WateringSoil class="widget-icon" />
|
||||||
|
<h3>Backflow Tests</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="widget-content">
|
||||||
|
<TodoChart
|
||||||
|
title="Backflow Tests"
|
||||||
|
:todoNumber="backflowsTodoNumber"
|
||||||
|
:completedNumber="backflowsCompletedNumber"
|
||||||
|
>
|
||||||
|
</TodoChart>
|
||||||
|
<button class="sidebar-button"
|
||||||
|
@click="navigateTo('/jobs')">
|
||||||
|
Late Balances
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
<!-- Curbing Widget -->
|
||||||
|
<Card class="widget-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="widget-header">
|
||||||
|
<SoilAlt class="widget-icon" />
|
||||||
|
<h3>Curbing</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="widget-content">
|
||||||
|
<TodoChart
|
||||||
|
title="Curbing"
|
||||||
|
:todoNumber="curbingTodoNumber"
|
||||||
|
:completedNumber="curbingCompletedNumber"
|
||||||
|
>
|
||||||
|
</TodoChart>
|
||||||
|
<button class="sidebar-button"
|
||||||
|
@click="navigateTo('/jobs')">
|
||||||
|
Curbing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
<!-- Hyrdoseeding Widget -->
|
||||||
|
<Card class="widget-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="widget-header">
|
||||||
|
<SoilAlt class="widget-icon" />
|
||||||
|
<h3>Hydroseeding</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="widget-content">
|
||||||
|
<TodoChart
|
||||||
|
title="Hydroseeding"
|
||||||
|
:todoNumber="hydroseedingTodoNumber"
|
||||||
|
:completedNumber="hydroseedingCompletedNumber"
|
||||||
|
>
|
||||||
|
</TodoChart>
|
||||||
|
<button class="sidebar-button"
|
||||||
|
@click="navigateTo('/jobs')">
|
||||||
|
Hydroseeding
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
<!-- Machines Widget -->
|
||||||
|
<Card class="widget-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="widget-header">
|
||||||
|
<Truck class="widget-icon" />
|
||||||
|
<h3>Machines</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="widget-content">
|
||||||
|
<TodoChart
|
||||||
|
title="Machines"
|
||||||
|
:todoNumber="machinesTodoNumber"
|
||||||
|
:completedNumber="machinesCompletedNumber"
|
||||||
|
>
|
||||||
|
</TodoChart>
|
||||||
|
<button class="sidebar-button"
|
||||||
|
@click="navigateTo('/jobs')">
|
||||||
|
Machines
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
<!-- Deliveries Widget -->
|
||||||
|
<Card class="widget-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="widget-header">
|
||||||
|
<Truck class="widget-icon" />
|
||||||
|
<h3>Deliveries</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="widget-content">
|
||||||
|
<TodoChart
|
||||||
|
title="Deliveries"
|
||||||
|
:todoNumber="deliveriesTodoNumber"
|
||||||
|
:completedNumber="delivieriesCompletedNumber"
|
||||||
|
>
|
||||||
|
</TodoChart>
|
||||||
|
<button class="sidebar-button"
|
||||||
|
@click="navigateTo('/jobs')">
|
||||||
|
Deliveries
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -209,17 +306,29 @@
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import Card from "primevue/card";
|
import Card from "primevue/card";
|
||||||
import Button from "primevue/button";
|
|
||||||
import Tag from "primevue/tag";
|
import Tag from "primevue/tag";
|
||||||
import { Calendar, Community, Hammer, PathArrowSolid, Clock, HistoricShield } from "@iconoir/vue";
|
import { Calendar, Community, Hammer, PathArrowSolid, Clock, Shield, ShieldSearch,
|
||||||
|
ClipboardCheck, DoubleCheck, CreditCard, CardNoAccess, ChatBubbleQuestion, Edit,
|
||||||
|
WateringSoil, Soil, Truck, SoilAlt } from "@iconoir/vue";
|
||||||
import DataUtils from "../../utils.js";
|
import DataUtils from "../../utils.js";
|
||||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||||
|
//import SimpleChart from "../common/SimpleChart.vue";
|
||||||
|
import TodoChart from "../common/TodoChart.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Dummy data from utils
|
// Dummy data from utils
|
||||||
const clientData = ref(DataUtils.dummyClientData);
|
const clientData = ref(DataUtils.dummyClientData);
|
||||||
const jobData = ref(DataUtils.dummyJobData);
|
const jobData = ref(DataUtils.dummyJobData);
|
||||||
|
const locatesTodoNumber = ref(45);
|
||||||
|
const locatesCompletedNumber = ref(5);
|
||||||
|
const permitsTodoNumber = ref(24);
|
||||||
|
const permitsCompletedNumber = ref(7);
|
||||||
|
const permitFinalizationsTodoNumber = ref(35);
|
||||||
|
const permitFinalizationsCompletedNumber = ref(2);
|
||||||
|
const warrantyTodoNumber = ref(0);
|
||||||
|
const warrantyCompletedNumber = ref(10);
|
||||||
|
|
||||||
const notifications = useNotificationStore();
|
const notifications = useNotificationStore();
|
||||||
|
|
||||||
// Computed values for dashboard metrics
|
// Computed values for dashboard metrics
|
||||||
|
|
@ -266,10 +375,11 @@ onMounted(() => {
|
||||||
transition:
|
transition:
|
||||||
transform 0.2s ease,
|
transform 0.2s ease,
|
||||||
box-shadow 0.2s ease;
|
box-shadow 0.2s ease;
|
||||||
|
background-color: var(--theme-surface-alt)
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-card:hover {
|
.widget-card:hover {
|
||||||
transform: translateY(-2px);
|
/*transform: translateY(-2px);*/
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,7 +391,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-icon {
|
.widget-icon {
|
||||||
color: rgb(69, 112, 101);
|
color: var(--theme-primary-strong);
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
@ -295,7 +405,8 @@ onMounted(() => {
|
||||||
.widget-content {
|
.widget-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
margin: 0;
|
||||||
|
/*gap: 15px;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric {
|
.metric {
|
||||||
|
|
@ -353,6 +464,10 @@ onMounted(() => {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-button {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.widgets-grid {
|
.widgets-grid {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue