add timesheets page and update homepage modules:

This commit is contained in:
Casey Wittrock 2025-10-28 00:53:05 -05:00
parent 0c921a3897
commit b70e08026d
4 changed files with 1235 additions and 30 deletions

View File

@ -45,6 +45,12 @@ class Api {
return data; return data;
} }
static async getTimesheetData() {
const data = DataUtils.dummyTimesheetData;
console.log("DEBUG: API - getTimesheetData result: ", data);
return data;
}
static async getDocsList(doctype, fields = []) { static async getDocsList(doctype, fields = []) {
const docs = await frappe.db.get_list(doctype, { fields }); const docs = await frappe.db.get_list(doctype, { fields });
console.log(`DEBUG: API - Fetched ${doctype} list: `, docs); console.log(`DEBUG: API - Fetched ${doctype} list: `, docs);

View File

@ -8,18 +8,18 @@
<template #header> <template #header>
<div class="widget-header"> <div class="widget-header">
<Calendar class="widget-icon" /> <Calendar class="widget-icon" />
<h3>Upcoming Schedule</h3> <h3>Service Calendar</h3>
</div> </div>
</template> </template>
<template #content> <template #content>
<div class="widget-content"> <div class="widget-content">
<div class="metric"> <div class="metric">
<span class="metric-number">8</span> <span class="metric-number">8</span>
<span class="metric-label">Appointments Today</span> <span class="metric-label">Services Scheduled Today</span>
</div> </div>
<div class="metric"> <div class="metric">
<span class="metric-number">15</span> <span class="metric-number">15</span>
<span class="metric-label">This Week</span> <span class="metric-label">Services This Week</span>
</div> </div>
<Button <Button
label="View Calendar" label="View Calendar"
@ -36,21 +36,21 @@
<template #header> <template #header>
<div class="widget-header"> <div class="widget-header">
<Community class="widget-icon" /> <Community class="widget-icon" />
<h3>Client Overview</h3> <h3>Client Contact List</h3>
</div> </div>
</template> </template>
<template #content> <template #content>
<div class="widget-content"> <div class="widget-content">
<div class="status-row"> <div class="status-row">
<Tag severity="success" value="5 Active" /> <Tag severity="success" value="5 Active Jobs" />
<Tag severity="warning" value="3 Pending" /> <Tag severity="warning" value="3 Pending Estimates" />
</div> </div>
<div class="metric"> <div class="metric">
<span class="metric-number">{{ clientData.length }}</span> <span class="metric-number">{{ clientData.length }}</span>
<span class="metric-label">Total Clients</span> <span class="metric-label">Total Client Records</span>
</div> </div>
<Button <Button
label="Manage Clients" label="View Client List"
size="small" size="small"
outlined outlined
@click="navigateTo('/clients')" @click="navigateTo('/clients')"
@ -64,7 +64,7 @@
<template #header> <template #header>
<div class="widget-header"> <div class="widget-header">
<Hammer class="widget-icon" /> <Hammer class="widget-icon" />
<h3>Active Jobs</h3> <h3>Job Management</h3>
</div> </div>
</template> </template>
<template #content> <template #content>
@ -77,10 +77,10 @@
<span class="metric-number">{{ <span class="metric-number">{{
jobData.filter((job) => job.overAllStatus === "in progress").length jobData.filter((job) => job.overAllStatus === "in progress").length
}}</span> }}</span>
<span class="metric-label">Active Jobs</span> <span class="metric-label">Jobs In Progress</span>
</div> </div>
<Button <Button
label="View Jobs" label="View All Jobs"
size="small" size="small"
outlined outlined
@click="navigateTo('/jobs')" @click="navigateTo('/jobs')"
@ -94,21 +94,21 @@
<template #header> <template #header>
<div class="widget-header"> <div class="widget-header">
<PathArrowSolid class="widget-icon" /> <PathArrowSolid class="widget-icon" />
<h3>Route Planning</h3> <h3>Service Routes</h3>
</div> </div>
</template> </template>
<template #content> <template #content>
<div class="widget-content"> <div class="widget-content">
<div class="metric"> <div class="metric">
<span class="metric-number">6</span> <span class="metric-number">6</span>
<span class="metric-label">Routes Today</span> <span class="metric-label">Active Routes Today</span>
</div> </div>
<div class="metric"> <div class="metric">
<span class="metric-number">45.2</span> <span class="metric-number">45.2</span>
<span class="metric-label">Avg. Miles/Route</span> <span class="metric-label">Avg. Miles per Route</span>
</div> </div>
<Button <Button
label="Plan Routes" label="View Routes"
size="small" size="small"
outlined outlined
@click="navigateTo('/routes')" @click="navigateTo('/routes')"
@ -122,18 +122,18 @@
<template #header> <template #header>
<div class="widget-header"> <div class="widget-header">
<Clock class="widget-icon" /> <Clock class="widget-icon" />
<h3>Time Tracking</h3> <h3>Employee Timesheets</h3>
</div> </div>
</template> </template>
<template #content> <template #content>
<div class="widget-content"> <div class="widget-content">
<div class="metric"> <div class="metric">
<span class="metric-number">32.5</span> <span class="metric-number">32.5</span>
<span class="metric-label">Hours This Week</span> <span class="metric-label">Total Hours This Week</span>
</div> </div>
<div class="status-row"> <div class="status-row">
<Tag severity="success" value="5 Completed" /> <Tag severity="success" value="5 Approved" />
<Tag severity="warning" value="2 Pending" /> <Tag severity="warning" value="2 Pending Approval" />
</div> </div>
<Button <Button
label="View Timesheets" label="View Timesheets"
@ -150,20 +150,21 @@
<template #header> <template #header>
<div class="widget-header"> <div class="widget-header">
<HistoricShield class="widget-icon" /> <HistoricShield class="widget-icon" />
<h3>Warranties</h3> <h3>Warranty Claims</h3>
</div> </div>
</template> </template>
<template #content> <template #content>
<div class="widget-content"> <div class="widget-content">
<div class="metric"> <div class="metric">
<span class="metric-number">23</span> <span class="metric-number">10</span>
<span class="metric-label">Active Warranties</span> <span class="metric-label">Open Claims</span>
</div> </div>
<div class="status-row"> <div class="status-row">
<Tag severity="danger" value="3 Expiring Soon" /> <Tag severity="success" value="3 Completed" />
<Tag severity="warning" value="4 In Progress" />
</div> </div>
<Button <Button
label="Manage Warranties" label="View Claims"
size="small" size="small"
outlined outlined
@click="navigateTo('/warranties')" @click="navigateTo('/warranties')"

View File

@ -1,9 +1,849 @@
<template lang=""> <template>
<div class="timesheets-page">
<div class="timesheets-header">
<h2>Employee Timesheets</h2>
<p class="timesheets-subtitle">
Track time, manage approvals, and monitor labor costs
</p>
</div>
<!-- Controls Section -->
<div class="controls-section">
<div class="filter-controls">
<div class="filter-group">
<label>Week Filter:</label>
<v-select
v-model="selectedWeek"
:items="weekOptions"
item-title="label"
item-value="value"
density="compact"
variant="outlined"
@update:modelValue="filterByWeek"
></v-select>
</div>
<div class="filter-group">
<label>Employee:</label>
<v-select
v-model="selectedEmployee"
:items="employeeOptions"
item-title="label"
item-value="value"
density="compact"
variant="outlined"
clearable
@update:modelValue="filterByEmployee"
></v-select>
</div>
<div class="filter-group">
<label>Status:</label>
<v-select
v-model="selectedStatus"
:items="statusOptions"
item-title="label"
item-value="value"
density="compact"
variant="outlined"
clearable
@update:modelValue="filterByStatus"
></v-select>
</div>
</div>
<div class="action-buttons">
<v-btn color="primary" @click="addNewTimesheet" prepend-icon="mdi-plus">
Add Timesheet
</v-btn>
<v-btn
color="success"
@click="bulkApprove"
prepend-icon="mdi-check-all"
:disabled="!hasSelectedForApproval"
>
Bulk Approve
</v-btn>
<v-btn color="info" @click="exportTimesheets" prepend-icon="mdi-download">
Export
</v-btn>
</div>
</div>
<!-- Summary Cards -->
<div class="summary-cards">
<v-card class="summary-card">
<v-card-text>
<div class="summary-content">
<v-icon color="primary" size="large">mdi-clock</v-icon>
<div class="summary-text">
<div class="summary-number">{{ totalHoursThisWeek }}</div>
<div class="summary-label">Hours This Week</div>
</div>
</div>
</v-card-text>
</v-card>
<v-card class="summary-card">
<v-card-text>
<div class="summary-content">
<v-icon color="warning" size="large">mdi-clock-alert</v-icon>
<div class="summary-text">
<div class="summary-number">{{ pendingApprovals }}</div>
<div class="summary-label">Pending Approval</div>
</div>
</div>
</v-card-text>
</v-card>
<v-card class="summary-card">
<v-card-text>
<div class="summary-content">
<v-icon color="success" size="large">mdi-currency-usd</v-icon>
<div class="summary-text">
<div class="summary-number">${{ totalLaborCost }}</div>
<div class="summary-label">Labor Cost</div>
</div>
</div>
</v-card-text>
</v-card>
<v-card class="summary-card">
<v-card-text>
<div class="summary-content">
<v-icon color="info" size="large">mdi-account-group</v-icon>
<div class="summary-text">
<div class="summary-number">{{ activeEmployees }}</div>
<div class="summary-label">Active Employees</div>
</div>
</div>
</v-card-text>
</v-card>
</div>
<!-- Main Timesheet Table -->
<div class="timesheets-table-container">
<DataTable
:data="filteredTableData"
:columns="columns"
:filters="filters"
@row-click="viewTimesheetDetails"
/>
</div>
<!-- Timesheet Details Modal -->
<v-dialog v-model="timesheetDialog" max-width="1000px" persistent>
<v-card v-if="selectedTimesheet">
<v-card-title class="d-flex justify-space-between align-center pa-4">
<div> <div>
<h2>TimeSheets Page</h2> <h3>Timesheet Details - {{ selectedTimesheet.timesheetId }}</h3>
<span class="text-subtitle-1 text-medium-emphasis">
{{ selectedTimesheet.employee }} -
{{ formatDate(selectedTimesheet.date) }}
</span>
</div>
<div class="d-flex align-center gap-2">
<v-chip :color="getStatusColor(selectedTimesheet.status)" size="small">
{{ selectedTimesheet.status.toUpperCase() }}
</v-chip>
<v-btn
icon="mdi-close"
variant="text"
@click="timesheetDialog = false"
></v-btn>
</div>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-0">
<div class="timesheet-details-container">
<!-- Left Panel - Basic Info -->
<div class="details-left-panel pa-4">
<h4 class="mb-3">Time & Job Information</h4>
<div class="detail-grid">
<div class="detail-item">
<v-icon class="mr-2" size="small">mdi-account</v-icon>
<span class="label">Employee:</span>
<span class="value">{{ selectedTimesheet.employee }}</span>
</div>
<div class="detail-item">
<v-icon class="mr-2" size="small">mdi-calendar</v-icon>
<span class="label">Date:</span>
<span class="value">{{
formatDate(selectedTimesheet.date)
}}</span>
</div>
<div class="detail-item">
<v-icon class="mr-2" size="small">mdi-briefcase</v-icon>
<span class="label">Job ID:</span>
<span class="value">{{ selectedTimesheet.jobId }}</span>
</div>
<div class="detail-item">
<v-icon class="mr-2" size="small">mdi-account-hard-hat</v-icon>
<span class="label">Customer:</span>
<span class="value">{{ selectedTimesheet.customer }}</span>
</div>
<div class="detail-item">
<v-icon class="mr-2" size="small">mdi-map-marker</v-icon>
<span class="label">Address:</span>
<span class="value">{{ selectedTimesheet.address }}</span>
</div>
<div class="detail-item">
<v-icon class="mr-2" size="small">mdi-wrench</v-icon>
<span class="label">Task:</span>
<span class="value">{{
selectedTimesheet.taskDescription
}}</span>
</div>
</div>
<!-- Time Details -->
<h4 class="mt-4 mb-3">Time Details</h4>
<div class="time-details">
<div class="time-row">
<span class="time-label">Clock In:</span>
<span class="time-value">{{ selectedTimesheet.clockIn }}</span>
</div>
<div class="time-row">
<span class="time-label">Clock Out:</span>
<span class="time-value">{{
selectedTimesheet.clockOut
}}</span>
</div>
<div class="time-row">
<span class="time-label">Break Time:</span>
<span class="time-value"
>{{ selectedTimesheet.breakTime }} min</span
>
</div>
<div class="time-row">
<span class="time-label">Regular Hours:</span>
<span class="time-value">{{
selectedTimesheet.regularHours
}}</span>
</div>
<div class="time-row">
<span class="time-label">Overtime Hours:</span>
<span class="time-value">{{
selectedTimesheet.overtimeHours
}}</span>
</div>
<div class="time-row total-row">
<span class="time-label">Total Hours:</span>
<span class="time-value">{{
selectedTimesheet.totalHours
}}</span>
</div>
</div>
<!-- Pay Details -->
<h4 class="mt-4 mb-3">Pay Calculation</h4>
<div class="pay-details">
<div class="pay-row">
<span class="pay-label">Regular Rate:</span>
<span class="pay-value"
>${{ selectedTimesheet.hourlyRate }}/hr</span
>
</div>
<div class="pay-row">
<span class="pay-label">Overtime Rate:</span>
<span class="pay-value"
>${{ selectedTimesheet.overtimeRate }}/hr</span
>
</div>
<div class="pay-row">
<span class="pay-label">Mileage:</span>
<span class="pay-value"
>{{ selectedTimesheet.mileage }} miles</span
>
</div>
<div class="pay-row total-row">
<span class="pay-label">Total Pay:</span>
<span class="pay-value"
>${{ selectedTimesheet.totalPay }}</span
>
</div>
</div>
</div>
<!-- Right Panel - Equipment, Materials, Notes -->
<div class="details-right-panel pa-4">
<h4 class="mb-3">Work Details</h4>
<!-- Equipment Used -->
<div class="work-section mb-4">
<h5 class="mb-2">Equipment Used:</h5>
<v-chip-group>
<v-chip
v-for="equipment in selectedTimesheet.equipment"
:key="equipment"
size="small"
color="primary"
variant="outlined"
>
{{ equipment }}
</v-chip>
</v-chip-group>
</div>
<!-- Materials Used -->
<div class="work-section mb-4">
<h5 class="mb-2">Materials Used:</h5>
<div class="materials-list">
<div
v-for="material in selectedTimesheet.materials"
:key="material"
class="material-item"
>
<v-icon size="small" class="mr-2"
>mdi-package-variant</v-icon
>
{{ material }}
</div>
</div>
</div>
<!-- Work Notes -->
<div class="work-section mb-4">
<h5 class="mb-2">Work Notes:</h5>
<div class="notes-content">
{{ selectedTimesheet.notes }}
</div>
</div>
<!-- Approval Status -->
<div class="approval-section">
<h5 class="mb-2">Approval Status:</h5>
<div class="approval-info">
<div v-if="selectedTimesheet.approved" class="approved-info">
<v-icon color="success" class="mr-2"
>mdi-check-circle</v-icon
>
<div>
<div class="approval-text">
Approved by {{ selectedTimesheet.approvedBy }}
</div>
<div class="approval-date">
{{ formatDate(selectedTimesheet.approvedDate) }}
</div>
</div>
</div>
<div v-else class="pending-info">
<v-icon color="warning" class="mr-2">mdi-clock</v-icon>
<span>Pending approval</span>
</div>
</div>
</div>
</div>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn color="grey" variant="outlined" @click="timesheetDialog = false">
Close
</v-btn>
<v-btn
v-if="!selectedTimesheet.approved"
color="success"
@click="approveTimesheet(selectedTimesheet)"
>
Approve
</v-btn>
<v-btn color="primary" @click="editTimesheet(selectedTimesheet)"> Edit </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div> </div>
</template> </template>
<script>
export default {}; <script setup>
import { ref, onMounted, computed } from "vue";
import DataTable from "../DataTable.vue";
import Api from "../../api";
import { FilterMatchMode } from "@primevue/core";
// Reactive data
const tableData = ref([]);
const filteredTableData = ref([]);
const timesheetDialog = ref(false);
const selectedTimesheet = ref(null);
// Filter controls
const selectedWeek = ref("current");
const selectedEmployee = ref(null);
const selectedStatus = ref(null);
// Filter options
const weekOptions = [
{ label: "Current Week", value: "current" },
{ label: "Last Week", value: "last" },
{ label: "Last 2 Weeks", value: "last2" },
{ label: "All Weeks", value: "all" },
];
const employeeOptions = ref([]);
const statusOptions = [
{ label: "Draft", value: "draft" },
{ label: "Submitted", value: "submitted" },
{ label: "Approved", value: "approved" },
];
// Table configuration
const columns = [
{ label: "Timesheet ID", fieldName: "timesheetId", type: "text", sortable: true },
{ label: "Employee", fieldName: "employee", type: "text", sortable: true, filterable: true },
{ label: "Date", fieldName: "date", type: "text", sortable: true },
{ label: "Job ID", fieldName: "jobId", type: "text", sortable: true },
{ label: "Customer", fieldName: "customer", type: "text", sortable: true },
{ label: "Total Hours", fieldName: "totalHours", type: "text", sortable: true },
{ label: "Status", fieldName: "status", type: "status", sortable: true },
{ label: "Total Pay", fieldName: "totalPayFormatted", type: "text", sortable: true },
{ label: "Actions", fieldName: "actions", type: "button", sortable: false },
];
const filters = {
employee: { value: null, matchMode: FilterMatchMode.CONTAINS },
};
// Computed properties
const totalHoursThisWeek = computed(() => {
const currentWeekData = getCurrentWeekTimesheets();
return currentWeekData
.reduce((total, timesheet) => total + timesheet.totalHours, 0)
.toFixed(1);
});
const pendingApprovals = computed(() => {
return tableData.value.filter((ts) => !ts.approved).length;
});
const totalLaborCost = computed(() => {
const currentWeekData = getCurrentWeekTimesheets();
const total = currentWeekData.reduce((sum, timesheet) => sum + timesheet.totalPay, 0);
return total.toLocaleString();
});
const activeEmployees = computed(() => {
const uniqueEmployees = new Set(tableData.value.map((ts) => ts.employee));
return uniqueEmployees.size;
});
const hasSelectedForApproval = computed(() => {
// In a real implementation, this would check selected rows
return pendingApprovals.value > 0;
});
// Methods
const getCurrentWeekTimesheets = () => {
const currentWeekStart = getCurrentWeekStart();
return tableData.value.filter((timesheet) => {
const timesheetDate = new Date(timesheet.date);
return timesheetDate >= currentWeekStart;
});
};
const getCurrentWeekStart = () => {
const now = new Date();
const dayOfWeek = now.getDay();
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust for Sunday
return new Date(now.setDate(diff));
};
const getWeekStart = (weeksAgo) => {
const now = new Date();
const dayOfWeek = now.getDay();
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1) - weeksAgo * 7;
return new Date(now.setDate(diff));
};
const filterByWeek = () => {
applyFilters();
};
const filterByEmployee = () => {
applyFilters();
};
const filterByStatus = () => {
applyFilters();
};
const applyFilters = () => {
let filtered = [...tableData.value];
// Week filter
if (selectedWeek.value !== "all") {
const weeksAgo =
selectedWeek.value === "current" ? 0 : selectedWeek.value === "last" ? 1 : 2;
const weekStart = getWeekStart(weeksAgo);
const weekEnd =
selectedWeek.value === "last2"
? new Date()
: new Date(weekStart.getTime() + 7 * 24 * 60 * 60 * 1000);
filtered = filtered.filter((timesheet) => {
const timesheetDate = new Date(timesheet.date);
return timesheetDate >= weekStart && timesheetDate < weekEnd;
});
}
// Employee filter
if (selectedEmployee.value) {
filtered = filtered.filter((ts) => ts.employee === selectedEmployee.value);
}
// Status filter
if (selectedStatus.value) {
filtered = filtered.filter((ts) => ts.status === selectedStatus.value);
}
filteredTableData.value = filtered;
};
const viewTimesheetDetails = (event) => {
const timesheetId = event.data.timesheetId;
const timesheet = tableData.value.find((ts) => ts.timesheetId === timesheetId);
if (timesheet) {
selectedTimesheet.value = timesheet;
timesheetDialog.value = true;
}
};
const addNewTimesheet = () => {
console.log("Add new timesheet clicked");
// TODO: Implement add timesheet functionality
};
const bulkApprove = () => {
console.log("Bulk approve clicked");
// TODO: Implement bulk approval functionality
};
const exportTimesheets = () => {
console.log("Export timesheets clicked");
// TODO: Implement export functionality
};
const approveTimesheet = (timesheet) => {
timesheet.approved = true;
timesheet.approvedBy = "Current User"; // In real app, get from user context
timesheet.approvedDate = new Date().toISOString().split("T")[0];
timesheet.status = "approved";
console.log("Approved timesheet:", timesheet.timesheetId);
};
const editTimesheet = (timesheet) => {
console.log("Edit timesheet:", timesheet.timesheetId);
// TODO: Implement edit functionality
};
const getStatusColor = (status) => {
switch (status?.toLowerCase()) {
case "approved":
return "success";
case "submitted":
return "warning";
case "draft":
return "grey";
default:
return "info";
}
};
const formatDate = (dateString) => {
if (!dateString) return "N/A";
const date = new Date(dateString);
return date.toLocaleDateString();
};
// Load data on component mount
onMounted(async () => {
try {
const data = await Api.getTimesheetData();
// Transform data for table display
tableData.value = data.map((timesheet) => ({
...timesheet,
totalPayFormatted: `$${timesheet.totalPay.toLocaleString()}`,
actions: "View Details",
}));
// Set up employee options
const uniqueEmployees = [...new Set(data.map((ts) => ts.employee))];
employeeOptions.value = uniqueEmployees.map((emp) => ({ label: emp, value: emp }));
// Apply initial filters
applyFilters();
console.log("Loaded timesheets:", tableData.value);
} catch (error) {
console.error("Error loading timesheets:", error);
}
});
</script> </script>
<style lang=""></style>
<style scoped>
.timesheets-page {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.timesheets-header {
margin-bottom: 24px;
}
.timesheets-header h2 {
margin-bottom: 8px;
color: #1976d2;
}
.timesheets-subtitle {
color: #666;
margin: 0;
}
.controls-section {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 24px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.filter-controls {
display: flex;
gap: 16px;
align-items: flex-end;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.filter-group label {
font-size: 0.9em;
color: #666;
font-weight: 500;
}
.action-buttons {
display: flex;
gap: 8px;
}
.summary-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.summary-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.summary-content {
display: flex;
align-items: center;
gap: 16px;
}
.summary-text {
display: flex;
flex-direction: column;
}
.summary-number {
font-size: 1.8em;
font-weight: 600;
color: #1976d2;
}
.summary-label {
font-size: 0.9em;
color: #666;
}
.timesheets-table-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.timesheet-details-container {
display: flex;
min-height: 600px;
}
.details-left-panel {
flex: 1;
border-right: 1px solid #e0e0e0;
background-color: #fafafa;
}
.details-right-panel {
flex: 1;
}
.detail-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
}
.detail-item .label {
font-weight: 500;
min-width: 80px;
color: #666;
}
.detail-item .value {
color: #333;
}
.time-details,
.pay-details {
background: white;
padding: 16px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.time-row,
.pay-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.time-row:last-child,
.pay-row:last-child {
border-bottom: none;
}
.total-row {
font-weight: 600;
background-color: #f8f9fa;
margin-top: 8px;
padding: 12px 8px;
border-radius: 4px;
}
.time-label,
.pay-label {
font-weight: 500;
color: #666;
}
.time-value,
.pay-value {
color: #333;
}
.work-section h5 {
color: #1976d2;
margin-bottom: 8px;
}
.materials-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.material-item {
display: flex;
align-items: center;
padding: 8px 12px;
background-color: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #1976d2;
font-size: 0.9em;
}
.notes-content {
background-color: #f8f9fa;
padding: 16px;
border-radius: 8px;
border-left: 4px solid #1976d2;
font-style: italic;
color: #555;
line-height: 1.5;
}
.approval-section {
background-color: #f8f9fa;
padding: 16px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.approved-info,
.pending-info {
display: flex;
align-items: center;
gap: 8px;
}
.approval-text {
font-weight: 500;
color: #333;
}
.approval-date {
font-size: 0.9em;
color: #666;
}
/* Responsive design */
@media (max-width: 768px) {
.controls-section {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.filter-controls {
flex-wrap: wrap;
}
.summary-cards {
grid-template-columns: repeat(2, 1fr);
}
.timesheet-details-container {
flex-direction: column;
}
.details-left-panel {
border-right: none;
border-bottom: 1px solid #e0e0e0;
}
}
</style>

View File

@ -1281,6 +1281,364 @@ class DataUtils {
systemType: "Commercial Irrigation", systemType: "Commercial Irrigation",
}, },
]; ];
static dummyTimesheetData = [
// Current week entries (Oct 25-31, 2025)
{
id: 1,
timesheetId: "TS-2025-001",
employee: "Mike Thompson",
employeeId: "EMP001",
date: "2025-10-25",
weekOf: "2025-10-21",
jobId: "JOB001",
customer: "John Doe",
address: "123 Lane Dr, Cityville, MN",
taskDescription: "Sprinkler System Installation - Zone 1-4",
clockIn: "07:30",
clockOut: "17:00",
breakTime: 30, // minutes
regularHours: 9.0,
overtimeHours: 0.5,
totalHours: 9.5,
status: "submitted",
approved: false,
approvedBy: null,
approvedDate: null,
hourlyRate: 28.5,
overtimeRate: 42.75,
totalPay: 292.88,
notes: "Completed main line installation and zone 1-4 heads",
equipment: ["Trencher", "Compactor", "Hand tools"],
mileage: 45,
materials: ["PVC Pipe - 200ft", "Sprinkler Heads - 15", "Valves - 4"],
},
{
id: 2,
timesheetId: "TS-2025-002",
employee: "Sarah Johnson",
employeeId: "EMP002",
date: "2025-10-25",
weekOf: "2025-10-21",
jobId: "JOB007",
customer: "Chris Taylor",
address: "159 Spruce Ct, Capital City, WA",
taskDescription: "Zone Expansion Install",
clockIn: "08:00",
clockOut: "16:30",
breakTime: 30,
regularHours: 8.0,
overtimeHours: 0,
totalHours: 8.0,
status: "approved",
approved: true,
approvedBy: "Lisa Anderson",
approvedDate: "2025-10-26",
hourlyRate: 26.0,
overtimeRate: 39.0,
totalPay: 208.0,
notes: "Added 3 new zones, tested all systems",
equipment: ["Boring machine", "Hand tools"],
mileage: 32,
materials: [
"Irrigation tubing - 150ft",
"Sprinkler Heads - 8",
"Control wire - 300ft",
],
},
{
id: 3,
timesheetId: "TS-2025-003",
employee: "David Martinez",
employeeId: "EMP003",
date: "2025-10-25",
weekOf: "2025-10-21",
jobId: "JOB012",
customer: "Sophia White",
address: "258 Palm Blvd, Riverside, AZ",
taskDescription: "Drip System Repair & Maintenance",
clockIn: "07:00",
clockOut: "15:30",
breakTime: 30,
regularHours: 8.0,
overtimeHours: 0,
totalHours: 8.0,
status: "draft",
approved: false,
approvedBy: null,
approvedDate: null,
hourlyRate: 24.5,
overtimeRate: 36.75,
totalPay: 196.0,
notes: "Repaired multiple leaks in drip lines, replaced emitters",
equipment: ["Hand tools", "Pressure gauge"],
mileage: 28,
materials: ["Drip tubing - 50ft", "Emitters - 25", "Fittings - misc"],
},
{
id: 4,
timesheetId: "TS-2025-004",
employee: "Chris Wilson",
employeeId: "EMP004",
date: "2025-10-24",
weekOf: "2025-10-21",
jobId: "JOB003",
customer: "Mike Johnson",
address: "789 Pine Rd, Villagetown, TX",
taskDescription: "Controller Programming & System Testing",
clockIn: "09:00",
clockOut: "17:30",
breakTime: 60,
regularHours: 7.5,
overtimeHours: 0,
totalHours: 7.5,
status: "submitted",
approved: false,
approvedBy: null,
approvedDate: null,
hourlyRate: 30.0,
overtimeRate: 45.0,
totalPay: 225.0,
notes: "Programmed new smart controller, tested all 8 zones",
equipment: ["Laptop", "Multimeter", "Hand tools"],
mileage: 42,
materials: ["Smart Controller - 1", "Wire nuts - 10"],
},
{
id: 5,
timesheetId: "TS-2025-005",
employee: "Lisa Anderson",
employeeId: "EMP005",
date: "2025-10-24",
weekOf: "2025-10-21",
jobId: "JOB017",
customer: "Ethan Walker",
address: "456 Walnut Rd, Meadowbrook, ND",
taskDescription: "Backflow Prevention Installation",
clockIn: "08:30",
clockOut: "16:00",
breakTime: 30,
regularHours: 7.0,
overtimeHours: 0,
totalHours: 7.0,
status: "approved",
approved: true,
approvedBy: "Mike Thompson",
approvedDate: "2025-10-25",
hourlyRate: 27.5,
overtimeRate: 41.25,
totalPay: 192.5,
notes: "Installed backflow preventer, pressure testing completed",
equipment: ["Pipe cutter", "Threading machine", "Pressure tester"],
mileage: 38,
materials: ["Backflow Preventer - 1", "Copper fittings - misc", "Solder"],
},
{
id: 6,
timesheetId: "TS-2025-006",
employee: "Mike Thompson",
employeeId: "EMP001",
date: "2025-10-23",
weekOf: "2025-10-21",
jobId: "JOB015",
customer: "Andrew Lewis",
address: "357 Dogwood Dr, Lakeview, VT",
taskDescription: "System Winterization",
clockIn: "07:30",
clockOut: "15:00",
breakTime: 30,
regularHours: 7.0,
overtimeHours: 0,
totalHours: 7.0,
status: "approved",
approved: true,
approvedBy: "Lisa Anderson",
approvedDate: "2025-10-24",
hourlyRate: 28.5,
overtimeRate: 42.75,
totalPay: 199.5,
notes: "Winterized entire system, drained all lines and components",
equipment: ["Air compressor", "Hand tools"],
mileage: 55,
materials: ["Compressed air", "Antifreeze - 2 gallons"],
},
// Previous week entries (Oct 14-20, 2025)
{
id: 7,
timesheetId: "TS-2025-007",
employee: "Sarah Johnson",
employeeId: "EMP002",
date: "2025-10-20",
weekOf: "2025-10-14",
jobId: "JOB010",
customer: "Olivia Thomas",
address: "147 Cypress St, Uptown, GA",
taskDescription: "Emergency Repair - Main Line Leak",
clockIn: "07:00",
clockOut: "18:30",
breakTime: 30,
regularHours: 8.0,
overtimeHours: 3.0,
totalHours: 11.0,
status: "approved",
approved: true,
approvedBy: "Mike Thompson",
approvedDate: "2025-10-21",
hourlyRate: 26.0,
overtimeRate: 39.0,
totalPay: 325.0,
notes: "Emergency call - repaired major main line break",
equipment: ["Excavator", "Pipe cutter", "Hand tools"],
mileage: 67,
materials: ["PVC Pipe - 20ft", "Couplings - 4", "Pipe cement"],
},
{
id: 8,
timesheetId: "TS-2025-008",
employee: "David Martinez",
employeeId: "EMP003",
date: "2025-10-19",
weekOf: "2025-10-14",
jobId: "JOB005",
customer: "David Wilson",
address: "654 Cedar Blvd, Borough, NY",
taskDescription: "Final System Testing & Cleanup",
clockIn: "08:00",
clockOut: "16:30",
breakTime: 30,
regularHours: 8.0,
overtimeHours: 0,
totalHours: 8.0,
status: "approved",
approved: true,
approvedBy: "Lisa Anderson",
approvedDate: "2025-10-20",
hourlyRate: 24.5,
overtimeRate: 36.75,
totalPay: 196.0,
notes: "Final system testing, site cleanup, customer walkthrough",
equipment: ["Hand tools", "Cleanup supplies"],
mileage: 25,
materials: ["Grass seed - 5 lbs", "Topsoil - 2 bags"],
},
{
id: 9,
timesheetId: "TS-2025-009",
employee: "Chris Wilson",
employeeId: "EMP004",
date: "2025-10-18",
weekOf: "2025-10-14",
jobId: "JOB009",
customer: "Joshua Anderson",
address: "852 Aspen Rd, Bigcity, NJ",
taskDescription: "Valve Replacement & Zone Testing",
clockIn: "08:30",
clockOut: "17:00",
breakTime: 30,
regularHours: 8.0,
overtimeHours: 0,
totalHours: 8.0,
status: "approved",
approved: true,
approvedBy: "Mike Thompson",
approvedDate: "2025-10-19",
hourlyRate: 30.0,
overtimeRate: 45.0,
totalPay: 240.0,
notes: "Replaced faulty zone 3 valve, tested all zones for proper operation",
equipment: ["Trenching shovel", "Hand tools", "Multimeter"],
mileage: 48,
materials: ["Zone Valve - 1", "Wire nuts - 5", "Electrical tape"],
},
{
id: 10,
timesheetId: "TS-2025-010",
employee: "Lisa Anderson",
employeeId: "EMP005",
date: "2025-10-17",
weekOf: "2025-10-14",
jobId: "JOB014",
customer: "Isabella Clark",
address: "753 Magnolia Ct, Hilltown, SC",
taskDescription: "Sprinkler Head Adjustments & Coverage Testing",
clockIn: "09:00",
clockOut: "16:30",
breakTime: 30,
regularHours: 7.0,
overtimeHours: 0,
totalHours: 7.0,
status: "approved",
approved: true,
approvedBy: "Sarah Johnson",
approvedDate: "2025-10-18",
hourlyRate: 27.5,
overtimeRate: 41.25,
totalPay: 192.5,
notes: "Adjusted all sprinkler heads for optimal coverage, marked areas needing attention",
equipment: ["Hand tools", "Measuring tape", "Spray paint"],
mileage: 33,
materials: ["Sprinkler Head Adjustment Tools", "Marking flags - 20"],
},
// Older entries for reporting
{
id: 11,
timesheetId: "TS-2025-011",
employee: "Mike Thompson",
employeeId: "EMP001",
date: "2025-10-16",
weekOf: "2025-10-14",
jobId: "JOB006",
customer: "Sarah Brown",
address: "987 Birch Ln, Metropolis, IL",
taskDescription: "Site Survey & Design Planning",
clockIn: "08:00",
clockOut: "15:30",
breakTime: 30,
regularHours: 7.0,
overtimeHours: 0,
totalHours: 7.0,
status: "approved",
approved: true,
approvedBy: "Lisa Anderson",
approvedDate: "2025-10-17",
hourlyRate: 28.5,
overtimeRate: 42.75,
totalPay: 199.5,
notes: "Completed site survey, created design plan for 4-zone expansion",
equipment: ["Measuring wheel", "Stake flags", "Camera"],
mileage: 40,
materials: ["Survey stakes - 50", "Flagging tape - 1 roll"],
},
{
id: 12,
timesheetId: "TS-2025-012",
employee: "Sarah Johnson",
employeeId: "EMP002",
date: "2025-10-15",
weekOf: "2025-10-14",
jobId: "MAINT-001",
customer: "Multiple Properties",
address: "Various - See route sheet",
taskDescription: "Routine Maintenance Route - North Valley",
clockIn: "07:00",
clockOut: "16:00",
breakTime: 30,
regularHours: 8.5,
overtimeHours: 0,
totalHours: 8.5,
status: "approved",
approved: true,
approvedBy: "Mike Thompson",
approvedDate: "2025-10-16",
hourlyRate: 26.0,
overtimeRate: 39.0,
totalPay: 221.0,
notes: "Completed maintenance on 8 properties - routine checks and minor adjustments",
equipment: ["Service truck", "Hand tools", "Pressure gauge"],
mileage: 85,
materials: ["Sprinkler heads - 3", "Nozzles - 5", "Wire nuts - 10"],
},
];
} }
export default DataUtils; export default DataUtils;