feat: enhance Banco de Horas management with new reporting features, including adjustments and inconsistencies tracking, advanced filters, and Excel export functionality
This commit is contained in:
163
apps/web/src/lib/components/ponto/TimePicker.svelte
Normal file
163
apps/web/src/lib/components/ponto/TimePicker.svelte
Normal file
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { ChevronUp, ChevronDown } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
onChange: (hours: number, minutes: number) => void;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let { hours, minutes, onChange, label, disabled = false }: Props = $props();
|
||||
|
||||
function incrementHours() {
|
||||
if (disabled) return;
|
||||
const newHours = hours + 1;
|
||||
onChange(newHours, minutes);
|
||||
}
|
||||
|
||||
function decrementHours() {
|
||||
if (disabled) return;
|
||||
const newHours = Math.max(0, hours - 1);
|
||||
onChange(newHours, minutes);
|
||||
}
|
||||
|
||||
function incrementMinutes() {
|
||||
if (disabled) return;
|
||||
const newMinutes = minutes + 15;
|
||||
if (newMinutes >= 60) {
|
||||
const extraHours = Math.floor(newMinutes / 60);
|
||||
const remainingMinutes = newMinutes % 60;
|
||||
onChange(hours + extraHours, remainingMinutes);
|
||||
} else {
|
||||
onChange(hours, newMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
function decrementMinutes() {
|
||||
if (disabled) return;
|
||||
const newMinutes = minutes - 15;
|
||||
if (newMinutes < 0) {
|
||||
if (hours > 0) {
|
||||
onChange(hours - 1, 60 + newMinutes);
|
||||
} else {
|
||||
onChange(0, 0);
|
||||
}
|
||||
} else {
|
||||
onChange(hours, newMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
function handleHoursInput(e: Event) {
|
||||
if (disabled) return;
|
||||
const target = e.target as HTMLInputElement;
|
||||
const value = parseInt(target.value) || 0;
|
||||
onChange(Math.max(0, value), minutes);
|
||||
}
|
||||
|
||||
function handleMinutesInput(e: Event) {
|
||||
if (disabled) return;
|
||||
const target = e.target as HTMLInputElement;
|
||||
const value = parseInt(target.value) || 0;
|
||||
const clampedValue = Math.max(0, Math.min(59, value));
|
||||
onChange(hours, clampedValue);
|
||||
}
|
||||
|
||||
const totalMinutes = $derived(hours * 60 + minutes);
|
||||
const displayText = $derived.by(() => {
|
||||
if (totalMinutes === 0) return '0h 0min';
|
||||
const h = Math.floor(totalMinutes / 60);
|
||||
const m = totalMinutes % 60;
|
||||
return `${h}h ${m}min`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="time-picker">
|
||||
{#if label}
|
||||
<div class="mb-2 block text-sm font-medium text-gray-700">{label}</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Horas -->
|
||||
<div class="flex flex-col items-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick={incrementHours}
|
||||
disabled={disabled}
|
||||
class="flex h-10 w-12 items-center justify-center rounded-t-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ChevronUp class="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={hours}
|
||||
oninput={handleHoursInput}
|
||||
disabled={disabled}
|
||||
class="h-14 w-12 border-x border-gray-300 bg-white text-center text-xl font-bold text-gray-900 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:bg-gray-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={decrementHours}
|
||||
disabled={disabled || hours === 0}
|
||||
class="flex h-10 w-12 items-center justify-center rounded-b-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ChevronDown class="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
<span class="mt-1 text-xs text-gray-500">horas</span>
|
||||
</div>
|
||||
|
||||
<!-- Separador -->
|
||||
<div class="text-2xl font-bold text-gray-400">:</div>
|
||||
|
||||
<!-- Minutos -->
|
||||
<div class="flex flex-col items-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick={incrementMinutes}
|
||||
disabled={disabled}
|
||||
class="flex h-10 w-12 items-center justify-center rounded-t-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ChevronUp class="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
value={minutes}
|
||||
oninput={handleMinutesInput}
|
||||
disabled={disabled}
|
||||
class="h-14 w-12 border-x border-gray-300 bg-white text-center text-xl font-bold text-gray-900 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:bg-gray-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={decrementMinutes}
|
||||
disabled={disabled || (hours === 0 && minutes === 0)}
|
||||
class="flex h-10 w-12 items-center justify-center rounded-b-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ChevronDown class="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
<span class="mt-1 text-xs text-gray-500">min</span>
|
||||
</div>
|
||||
|
||||
<!-- Total -->
|
||||
<div class="ml-4 flex flex-col items-center justify-center rounded-lg bg-primary/10 px-4 py-2">
|
||||
<span class="text-xs text-gray-600">Total</span>
|
||||
<span class="text-lg font-bold text-primary">{displayText}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.time-picker input[type='number']::-webkit-inner-spin-button,
|
||||
.time-picker input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time-picker input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user