1052 lines
26 KiB
Vue

<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="tableData"
:columns="columns"
tableName="timesheets"
:lazy="true"
:totalRecords="totalRecords"
:loading="isLoading"
:onLazyLoad="handleLazyLoad"
@lazy-load="handleLazyLoad"
@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>
<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>
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import DataTable from "../common/DataTable.vue";
import Api from "../../api";
import { FilterMatchMode } from "@primevue/core";
import { useLoadingStore } from "../../stores/loading";
import { usePaginationStore } from "../../stores/pagination";
import { useFiltersStore } from "../../stores/filters";
import { useNotificationStore } from "../../stores/notifications-primevue";
const notifications = useNotificationStore();
const loadingStore = useLoadingStore();
const paginationStore = usePaginationStore();
const filtersStore = useFiltersStore();
// Reactive data
const tableData = ref([]);
const totalRecords = ref(0);
const isLoading = ref(false);
const timesheetDialog = ref(false);
const selectedTimesheet = ref(null);
const allTimesheetData = ref([]); // Store all data for filtering and calculations
// 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 },
];
// Handle lazy loading events from DataTable
const handleLazyLoad = async (event) => {
console.log("TimeSheets page - handling lazy load:", event);
try {
isLoading.value = true;
// Get pagination parameters
const paginationParams = {
page: event.page || 0,
pageSize: event.rows || 10,
sortField: event.sortField,
sortOrder: event.sortOrder,
};
// Get filters (convert PrimeVue format to API format)
const filters = {};
if (event.filters) {
Object.keys(event.filters).forEach((key) => {
if (key !== "global" && event.filters[key] && event.filters[key].value) {
filters[key] = event.filters[key];
}
});
}
// Apply additional filters from the controls
const additionalFilters = {
week: selectedWeek.value,
employee: selectedEmployee.value,
status: selectedStatus.value,
};
// Combine filters
const combinedFilters = { ...filters, ...additionalFilters };
// Check cache first
const cacheKey = `${JSON.stringify(combinedFilters)}`;
const cachedData = paginationStore.getCachedPage(
"timesheets",
paginationParams.page,
paginationParams.pageSize,
paginationParams.sortField,
paginationParams.sortOrder,
combinedFilters,
);
if (cachedData) {
// Use cached data
tableData.value = cachedData.records;
totalRecords.value = cachedData.totalRecords;
allTimesheetData.value = cachedData.allData || [];
paginationStore.setTotalRecords("timesheets", cachedData.totalRecords);
console.log("Loaded from cache:", {
records: cachedData.records.length,
total: cachedData.totalRecords,
page: paginationParams.page + 1,
});
return;
}
console.log("Making API call with:", { paginationParams, combinedFilters });
// For now, use existing API but we should create a paginated version
// TODO: Create Api.getPaginatedTimesheetData() method
const data = await Api.getTimesheetData();
// Store all data for calculations
allTimesheetData.value = data;
// Apply local filtering based on controls
let filteredData = data;
// Week filter
if (selectedWeek.value !== "all") {
const currentWeekStart = getCurrentWeekStart();
let weekStart;
switch (selectedWeek.value) {
case "current":
weekStart = currentWeekStart;
break;
case "last":
weekStart = getWeekStart(1);
break;
case "last2":
weekStart = getWeekStart(2);
break;
default:
weekStart = null;
}
if (weekStart) {
filteredData = filteredData.filter((timesheet) => {
const timesheetDate = new Date(timesheet.date);
return timesheetDate >= weekStart;
});
}
}
// Employee filter
if (selectedEmployee.value) {
filteredData = filteredData.filter((ts) => ts.employee === selectedEmployee.value);
}
// Status filter
if (selectedStatus.value) {
filteredData = filteredData.filter((ts) => ts.status === selectedStatus.value);
}
// Simulate pagination on filtered data
const startIndex = paginationParams.page * paginationParams.pageSize;
const endIndex = startIndex + paginationParams.pageSize;
const paginatedData = filteredData.slice(startIndex, endIndex);
// Update local state
tableData.value = paginatedData;
totalRecords.value = filteredData.length;
// Update pagination store with new total
paginationStore.setTotalRecords("timesheets", filteredData.length);
// Cache the result
paginationStore.setCachedPage(
"timesheets",
paginationParams.page,
paginationParams.pageSize,
paginationParams.sortField,
paginationParams.sortOrder,
combinedFilters,
{
records: paginatedData,
totalRecords: filteredData.length,
allData: data,
},
);
console.log("Loaded from API:", {
records: paginatedData.length,
total: filteredData.length,
page: paginationParams.page + 1,
});
} catch (error) {
console.error("Error loading timesheet data:", error);
tableData.value = [];
totalRecords.value = 0;
allTimesheetData.value = [];
} finally {
isLoading.value = false;
}
};
// Computed properties
const totalHoursThisWeek = computed(() => {
const currentWeekData = getCurrentWeekTimesheets();
return currentWeekData
.reduce((total, timesheet) => total + timesheet.totalHours, 0)
.toFixed(1);
});
const pendingApprovals = computed(() => {
return allTimesheetData.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(allTimesheetData.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
// Methods
const getCurrentWeekTimesheets = () => {
const currentWeekStart = getCurrentWeekStart();
return allTimesheetData.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 = () => {
// Reset to first page when filters change
paginationStore.resetToFirstPage("timesheets");
triggerLazyLoad();
};
const filterByEmployee = () => {
// Reset to first page when filters change
paginationStore.resetToFirstPage("timesheets");
triggerLazyLoad();
};
const filterByStatus = () => {
// Reset to first page when filters change
paginationStore.resetToFirstPage("timesheets");
triggerLazyLoad();
};
const triggerLazyLoad = () => {
const paginationParams = paginationStore.getPaginationParams("timesheets");
const filters = filtersStore.getTableFilters("timesheets");
const lazyEvent = {
page: paginationParams.page,
rows: paginationParams.pageSize,
first: paginationParams.offset,
sortField: paginationParams.sortField,
sortOrder: paginationParams.sortOrder,
filters: filters,
};
console.log("Triggering lazy load with:", lazyEvent);
handleLazyLoad(lazyEvent);
};
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 = allTimesheetData.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 () => {
notifications.addWarning("Timesheets page coming soon!");
try {
// Initialize pagination and filters
paginationStore.initializeTablePagination("timesheets", { rows: 10 });
filtersStore.initializeTableFilters("timesheets", columns);
// Load data to set up employee options
const data = await Api.getTimesheetData();
allTimesheetData.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 }));
// Load first page
const initialPagination = paginationStore.getTablePagination("timesheets");
const initialFilters = filtersStore.getTableFilters("timesheets");
await handleLazyLoad({
page: initialPagination.page,
rows: initialPagination.rows,
first: initialPagination.first,
sortField: initialPagination.sortField,
sortOrder: initialPagination.sortOrder,
filters: initialFilters,
});
console.log("Loaded timesheets:", allTimesheetData.value.length);
} catch (error) {
console.error("Error loading timesheets:", error);
}
});
</script>
<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>