feat: enhance attendance tracking by adding user-specific attendance data and conditional rendering in dashboard and analysis components; introduce AttendanceCard component for better UI management
This commit is contained in:
parent
c378036691
commit
a9b02a9bca
@ -261,12 +261,10 @@ public function permissions(): array
|
|||||||
|
|
||||||
self::ADMIN_BAHAN_BAKU => [
|
self::ADMIN_BAHAN_BAKU => [
|
||||||
Permission::DASHBOARD_VIEW,
|
Permission::DASHBOARD_VIEW,
|
||||||
Permission::DASHBOARD_ATTENDANCE,
|
|
||||||
|
|
||||||
Permission::ANALYSIS_VIEW,
|
Permission::ANALYSIS_VIEW,
|
||||||
Permission::ANALYSIS_ATTENDANCE,
|
Permission::ANALYSIS_ATTENDANCE,
|
||||||
Permission::ANALYSIS_RAW_MATERIALS,
|
Permission::ANALYSIS_RAW_MATERIALS,
|
||||||
Permission::ANALYSIS_TOP_SUPPLIERS,
|
|
||||||
|
|
||||||
Permission::ATTENDANCES_VIEW,
|
Permission::ATTENDANCES_VIEW,
|
||||||
Permission::ATTENDANCES_CREATE,
|
Permission::ATTENDANCES_CREATE,
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Services\System\AnalysisService;
|
use App\Services\System\AnalysisService;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
@ -20,12 +21,17 @@ public function index(Request $request): Response
|
|||||||
$startDate = $request->query('start_date') ? Carbon::parse($request->query('start_date'))->startOfDay() : null;
|
$startDate = $request->query('start_date') ? Carbon::parse($request->query('start_date'))->startOfDay() : null;
|
||||||
$endDate = $request->query('end_date') ? Carbon::parse($request->query('end_date'))->endOfDay() : null;
|
$endDate = $request->query('end_date') ? Carbon::parse($request->query('end_date'))->endOfDay() : null;
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
$isManager = $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
|
||||||
|
|
||||||
return Inertia::render('admin/Analysis', [
|
return Inertia::render('admin/Analysis', [
|
||||||
'filters' => [
|
'filters' => [
|
||||||
'start_date' => $request->query('start_date', ''),
|
'start_date' => $request->query('start_date', ''),
|
||||||
'end_date' => $request->query('end_date', ''),
|
'end_date' => $request->query('end_date', ''),
|
||||||
],
|
],
|
||||||
'attendance' => $this->analysisService->getAttendance(),
|
'attendance' => $this->analysisService->getAttendance(),
|
||||||
|
'myAttendance' => $user ? $this->analysisService->getMyAttendance($user, $startDate, $endDate) : null,
|
||||||
|
'isManager' => $isManager,
|
||||||
'cashOverview' => $this->analysisService->getCashOverview(),
|
'cashOverview' => $this->analysisService->getCashOverview(),
|
||||||
'rawMaterialStock' => $this->analysisService->getRawMaterialStock(),
|
'rawMaterialStock' => $this->analysisService->getRawMaterialStock(),
|
||||||
'productStock' => $this->analysisService->getProductStock(),
|
'productStock' => $this->analysisService->getProductStock(),
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Services\System\DashboardService;
|
use App\Services\System\DashboardService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
|
|
||||||
@ -13,8 +14,10 @@ public function __construct(
|
|||||||
private readonly DashboardService $dashboardService,
|
private readonly DashboardService $dashboardService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
return Inertia::render('admin/Dashboard', [
|
return Inertia::render('admin/Dashboard', [
|
||||||
'attendance' => $this->dashboardService->getAttendance(),
|
'attendance' => $this->dashboardService->getAttendance(),
|
||||||
'cashOverview' => $this->dashboardService->getCashOverview(),
|
'cashOverview' => $this->dashboardService->getCashOverview(),
|
||||||
@ -22,6 +25,16 @@ public function index(): Response
|
|||||||
'expenseSummary' => $this->dashboardService->getExpenseSummary(),
|
'expenseSummary' => $this->dashboardService->getExpenseSummary(),
|
||||||
|
|
||||||
'orderStats' => $this->dashboardService->getOrderStats(),
|
'orderStats' => $this->dashboardService->getOrderStats(),
|
||||||
|
|
||||||
|
'todayAttendance' => $user
|
||||||
|
? $this->dashboardService->getTodayAttendanceForUser($user)
|
||||||
|
: null,
|
||||||
|
'isOnLeave' => $user
|
||||||
|
? $this->dashboardService->isOnLeaveTodayForUser($user)
|
||||||
|
: false,
|
||||||
|
'canCheckIn' => $user
|
||||||
|
? $this->dashboardService->canCheckInForUser($user)
|
||||||
|
: false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
use App\Models\ProductVariant;
|
use App\Models\ProductVariant;
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
use App\Models\RawMaterialPrice;
|
use App\Models\RawMaterialPrice;
|
||||||
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class AnalysisService
|
class AnalysisService
|
||||||
@ -47,6 +48,69 @@ public function getAttendance(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMyAttendance(User $user, ?Carbon $startDate = null, ?Carbon $endDate = null): array
|
||||||
|
{
|
||||||
|
$employee = $user->employee;
|
||||||
|
|
||||||
|
if ($employee === null) {
|
||||||
|
return [
|
||||||
|
'total_days' => 0,
|
||||||
|
'present_days' => 0,
|
||||||
|
'absent_days' => 0,
|
||||||
|
'leave_days' => 0,
|
||||||
|
'percentage' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = $startDate ?? Carbon::today()->startOfMonth();
|
||||||
|
$end = $endDate ?? Carbon::today()->endOfMonth();
|
||||||
|
|
||||||
|
$totalDays = 0;
|
||||||
|
$presentDays = 0;
|
||||||
|
$current = $start->copy()->startOfDay();
|
||||||
|
|
||||||
|
while ($current->lte($end)) {
|
||||||
|
if ($current->isWeekday()) {
|
||||||
|
$totalDays++;
|
||||||
|
|
||||||
|
$hasAttendance = Attendance::query()
|
||||||
|
->where('employee_id', $employee->id)
|
||||||
|
->whereDate('attendance_date', $current)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($hasAttendance) {
|
||||||
|
$presentDays++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$current->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
$leaveDays = LeaveRequest::query()
|
||||||
|
->approved()
|
||||||
|
->where('employee_id', $employee->id)
|
||||||
|
->where('start_date', '<=', $end->toDateString())
|
||||||
|
->where('end_date', '>=', $start->toDateString())
|
||||||
|
->get()
|
||||||
|
->sum(fn ($leave) => max(
|
||||||
|
0,
|
||||||
|
min($leave->end_date, $end->toDateString())
|
||||||
|
- max($leave->start_date, $start->toDateString())
|
||||||
|
) / 86400 + 1);
|
||||||
|
|
||||||
|
$leaveDays = (int) $leaveDays;
|
||||||
|
$absentDays = max(0, $totalDays - $presentDays - $leaveDays);
|
||||||
|
$percentage = $totalDays > 0 ? round(($presentDays / $totalDays) * 100) : 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_days' => $totalDays,
|
||||||
|
'present_days' => $presentDays,
|
||||||
|
'absent_days' => $absentDays,
|
||||||
|
'leave_days' => $leaveDays,
|
||||||
|
'percentage' => $percentage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function getCashOverview(): array
|
public function getCashOverview(): array
|
||||||
{
|
{
|
||||||
$totalBalance = CashAccount::query()->sum('balance');
|
$totalBalance = CashAccount::query()->sum('balance');
|
||||||
|
|||||||
@ -14,10 +14,52 @@
|
|||||||
use App\Models\LeaveRequest;
|
use App\Models\LeaveRequest;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class DashboardService
|
class DashboardService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
public function getTodayAttendanceForUser(User $user): ?array
|
||||||
|
{
|
||||||
|
$employee = $user->employee;
|
||||||
|
|
||||||
|
if ($employee === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attendance = Attendance::query()
|
||||||
|
->with('media')
|
||||||
|
->where('employee_id', $employee->id)
|
||||||
|
->whereDate('attendance_date', today())
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $attendance?->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOnLeaveTodayForUser(User $user): bool
|
||||||
|
{
|
||||||
|
$employee = $user->employee;
|
||||||
|
|
||||||
|
if ($employee === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LeaveRequest::query()
|
||||||
|
->approved()
|
||||||
|
->where('employee_id', $employee->id)
|
||||||
|
->whereDate('start_date', '<=', today())
|
||||||
|
->whereDate('end_date', '>=', today())
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canCheckInForUser(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->can('attendances.create') && $user->employee !== null;
|
||||||
|
}
|
||||||
|
|
||||||
public function getAttendance(): array
|
public function getAttendance(): array
|
||||||
{
|
{
|
||||||
$totalEmployees = Employee::query()->count();
|
$totalEmployees = Employee::query()->count();
|
||||||
|
|||||||
108
resources/js/components/card/AttendanceCard.vue
Normal file
108
resources/js/components/card/AttendanceCard.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Clock, LogIn, LogOut } from '@lucide/vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import type { TodayAttendance } from '@/types/attendance';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
todayAttendance: TodayAttendance;
|
||||||
|
isOnLeave: boolean;
|
||||||
|
canCheckIn: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'check-in': [];
|
||||||
|
'check-out': [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const canCheckInToday = computed(() => props.canCheckIn && props.todayAttendance === null && !props.isOnLeave);
|
||||||
|
const canCheckOutToday = computed(() => (
|
||||||
|
props.canCheckIn
|
||||||
|
&& !props.isOnLeave
|
||||||
|
&& props.todayAttendance !== null
|
||||||
|
&& props.todayAttendance.check_out_at === null
|
||||||
|
));
|
||||||
|
|
||||||
|
const todayStatusLabel = computed(() => {
|
||||||
|
if (props.isOnLeave) {
|
||||||
|
return 'Sedang cuti';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.todayAttendance) {
|
||||||
|
return 'Belum presensi';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.todayAttendance.check_out_at) {
|
||||||
|
return 'Selesai';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Sudah masuk';
|
||||||
|
});
|
||||||
|
|
||||||
|
const todayStatusVariant = computed(() => {
|
||||||
|
if (props.isOnLeave) {
|
||||||
|
return 'secondary' as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.todayAttendance) {
|
||||||
|
return 'secondary' as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.todayAttendance.check_out_at) {
|
||||||
|
return 'default' as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'outline' as const;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card v-if="canCheckIn">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2">
|
||||||
|
<Clock class="size-5" />
|
||||||
|
Presensi Hari Ini
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Gunakan kamera perangkat untuk presensi masuk dan pulang.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<Badge :variant="todayStatusVariant">
|
||||||
|
{{ todayStatusLabel }}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<span v-if="isOnLeave" class="text-muted-foreground text-sm">
|
||||||
|
Anda sedang dalam masa cuti. Presensi tidak diperlukan.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="todayAttendance?.check_in_at_formatted" class="text-muted-foreground text-sm">
|
||||||
|
Masuk: {{ todayAttendance.check_in_at_formatted }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="todayAttendance?.check_out_at_formatted" class="text-muted-foreground text-sm">
|
||||||
|
Pulang: {{ todayAttendance.check_out_at_formatted }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="todayAttendance?.work_duration_formatted" class="text-muted-foreground text-sm">
|
||||||
|
Durasi: {{ todayAttendance.work_duration_formatted }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<Button :disabled="!canCheckInToday" @click="emit('check-in')">
|
||||||
|
<LogIn class="size-4" />
|
||||||
|
Presensi Masuk
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="outline" :disabled="!canCheckOutToday" @click="emit('check-out')">
|
||||||
|
<LogOut class="size-4" />
|
||||||
|
Presensi Pulang
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -35,6 +35,14 @@ const props = defineProps<{
|
|||||||
absent: number;
|
absent: number;
|
||||||
on_leave: number;
|
on_leave: number;
|
||||||
};
|
};
|
||||||
|
myAttendance: {
|
||||||
|
total_days: number;
|
||||||
|
present_days: number;
|
||||||
|
absent_days: number;
|
||||||
|
leave_days: number;
|
||||||
|
percentage: number;
|
||||||
|
} | null;
|
||||||
|
isManager: boolean;
|
||||||
cashOverview: {
|
cashOverview: {
|
||||||
total_balance: number;
|
total_balance: number;
|
||||||
total_transactions: number;
|
total_transactions: number;
|
||||||
@ -354,7 +362,7 @@ watch([startDate, endDate], () => {
|
|||||||
|
|
||||||
<!-- Kehadiran & Kas Toko -->
|
<!-- Kehadiran & Kas Toko -->
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<StatCard v-if="can('analysis.attendance')" title="Kehadiran" :icon="UserCheck" main-label="Total Karyawan"
|
<StatCard v-if="can('analysis.attendance') && isManager" title="Kehadiran" :icon="UserCheck" main-label="Total Karyawan"
|
||||||
:main-value="attendance.total_employees" :sub-label="attendance.percentage + '% hadir'" :items="[
|
:main-value="attendance.total_employees" :sub-label="attendance.percentage + '% hadir'" :items="[
|
||||||
{
|
{
|
||||||
label: 'Hadir',
|
label: 'Hadir',
|
||||||
@ -370,6 +378,22 @@ watch([startDate, endDate], () => {
|
|||||||
},
|
},
|
||||||
]" />
|
]" />
|
||||||
|
|
||||||
|
<StatCard v-if="can('analysis.attendance') && !isManager && myAttendance" title="Kehadiran Saya" :icon="UserCheck" main-label="Hari Kerja"
|
||||||
|
:main-value="myAttendance.total_days" :sub-label="myAttendance.percentage + '% hadir'" :items="[
|
||||||
|
{
|
||||||
|
label: 'Hadir',
|
||||||
|
value: myAttendance.present_days,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tidak Hadir',
|
||||||
|
value: myAttendance.absent_days,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cuti',
|
||||||
|
value: myAttendance.leave_days,
|
||||||
|
},
|
||||||
|
]" />
|
||||||
|
|
||||||
<StatCard v-if="can('analysis.cash')" title="Kas Toko" :icon="Banknote" main-label="Total Saldo"
|
<StatCard v-if="can('analysis.cash')" title="Kas Toko" :icon="Banknote" main-label="Total Saldo"
|
||||||
:main-value="'Rp' + formatRupiah(cashOverview.total_balance)" :sub-label="cashOverview.total_transactions + ' transaksi'
|
:main-value="'Rp' + formatRupiah(cashOverview.total_balance)" :sub-label="cashOverview.total_transactions + ' transaksi'
|
||||||
" :items="[
|
" :items="[
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
} from '@lucide/vue';
|
} from '@lucide/vue';
|
||||||
import { VisDonut, VisSingleContainer } from '@unovis/vue';
|
import { VisDonut, VisSingleContainer } from '@unovis/vue';
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import AttendanceCard from '@/components/card/AttendanceCard.vue';
|
||||||
import StatCard from '@/components/card/StatCard.vue';
|
import StatCard from '@/components/card/StatCard.vue';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -24,6 +25,8 @@ import { ChartContainer } from '@/components/ui/chart';
|
|||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { formatRupiah } from '@/lib/rupiah';
|
import { formatRupiah } from '@/lib/rupiah';
|
||||||
|
import type { TodayAttendance } from '@/types/attendance';
|
||||||
|
import AttendanceWebcamModal from '@/pages/admin/hr/attendances/form/AttendanceWebcamModal.vue';
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
attendance: {
|
attendance: {
|
||||||
@ -79,12 +82,28 @@ interface DashboardProps {
|
|||||||
count: number;
|
count: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
todayAttendance: TodayAttendance;
|
||||||
|
isOnLeave: boolean;
|
||||||
|
canCheckIn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<DashboardProps>();
|
const props = defineProps<DashboardProps>();
|
||||||
|
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
|
|
||||||
|
const webcamModalOpen = ref(false);
|
||||||
|
const webcamMode = ref<'check-in' | 'check-out'>('check-in');
|
||||||
|
|
||||||
|
function openCheckInModal() {
|
||||||
|
webcamMode.value = 'check-in';
|
||||||
|
webcamModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCheckOutModal() {
|
||||||
|
webcamMode.value = 'check-out';
|
||||||
|
webcamModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
const currentTime = ref(new Date());
|
const currentTime = ref(new Date());
|
||||||
let timer: ReturnType<typeof setInterval>;
|
let timer: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
@ -225,6 +244,14 @@ const marketingChartConfig = computed(() => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<AttendanceCard
|
||||||
|
:today-attendance="todayAttendance"
|
||||||
|
:is-on-leave="isOnLeave"
|
||||||
|
:can-check-in="canCheckIn"
|
||||||
|
@check-in="openCheckInModal"
|
||||||
|
@check-out="openCheckOutModal"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<StatCard v-if="can('dashboard.attendance')" title="Kehadiran" :icon="UserCheck"
|
<StatCard v-if="can('dashboard.attendance')" title="Kehadiran" :icon="UserCheck"
|
||||||
main-label="Total Karyawan" :main-value="attendance.total_employees"
|
main-label="Total Karyawan" :main-value="attendance.total_employees"
|
||||||
@ -412,5 +439,7 @@ const marketingChartConfig = computed(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AttendanceWebcamModal v-if="canCheckIn" v-model:open="webcamModalOpen" :mode="webcamMode" />
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { Clock, LogIn, LogOut } from '@lucide/vue';
|
import { ref } from 'vue';
|
||||||
import { computed, ref } from 'vue';
|
import AttendanceCard from '@/components/card/AttendanceCard.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { AttendanceListItem, CalendarRange, TodayAttendance } from '@/types/attendance';
|
import type { AttendanceListItem, CalendarRange, TodayAttendance } from '@/types/attendance';
|
||||||
import AttendanceWebcamModal from './form/AttendanceWebcamModal.vue';
|
import AttendanceWebcamModal from './form/AttendanceWebcamModal.vue';
|
||||||
@ -22,46 +20,6 @@ const props = defineProps<{
|
|||||||
const webcamModalOpen = ref(false);
|
const webcamModalOpen = ref(false);
|
||||||
const webcamMode = ref<'check-in' | 'check-out'>('check-in');
|
const webcamMode = ref<'check-in' | 'check-out'>('check-in');
|
||||||
|
|
||||||
const canCheckInToday = computed(() => props.canCheckIn && props.todayAttendance === null && !props.isOnLeave);
|
|
||||||
const canCheckOutToday = computed(() => (
|
|
||||||
props.canCheckIn
|
|
||||||
&& !props.isOnLeave
|
|
||||||
&& props.todayAttendance !== null
|
|
||||||
&& props.todayAttendance.check_out_at === null
|
|
||||||
));
|
|
||||||
|
|
||||||
const todayStatusLabel = computed(() => {
|
|
||||||
if (props.isOnLeave) {
|
|
||||||
return 'Sedang cuti';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!props.todayAttendance) {
|
|
||||||
return 'Belum presensi';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.todayAttendance.check_out_at) {
|
|
||||||
return 'Selesai';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Sudah masuk';
|
|
||||||
});
|
|
||||||
|
|
||||||
const todayStatusVariant = computed(() => {
|
|
||||||
if (props.isOnLeave) {
|
|
||||||
return 'secondary' as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!props.todayAttendance) {
|
|
||||||
return 'secondary' as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.todayAttendance.check_out_at) {
|
|
||||||
return 'default' as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'outline' as const;
|
|
||||||
});
|
|
||||||
|
|
||||||
function openCheckInModal() {
|
function openCheckInModal() {
|
||||||
webcamMode.value = 'check-in';
|
webcamMode.value = 'check-in';
|
||||||
webcamModalOpen.value = true;
|
webcamModalOpen.value = true;
|
||||||
@ -86,52 +44,13 @@ function openCheckOutModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card v-if="canCheckIn">
|
<AttendanceCard
|
||||||
<CardHeader>
|
:today-attendance="todayAttendance"
|
||||||
<CardTitle class="flex items-center gap-2">
|
:is-on-leave="isOnLeave"
|
||||||
<Clock class="size-5" />
|
:can-check-in="canCheckIn"
|
||||||
Presensi Hari Ini
|
@check-in="openCheckInModal"
|
||||||
</CardTitle>
|
@check-out="openCheckOutModal"
|
||||||
<CardDescription>
|
/>
|
||||||
Gunakan kamera perangkat untuk presensi masuk dan pulang.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="space-y-4">
|
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
|
||||||
<Badge :variant="todayStatusVariant">
|
|
||||||
{{ todayStatusLabel }}
|
|
||||||
</Badge>
|
|
||||||
|
|
||||||
<span v-if="isOnLeave" class="text-muted-foreground text-sm">
|
|
||||||
Anda sedang dalam masa cuti. Presensi tidak diperlukan.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="todayAttendance?.check_in_at_formatted" class="text-muted-foreground text-sm">
|
|
||||||
Masuk: {{ todayAttendance.check_in_at_formatted }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="todayAttendance?.check_out_at_formatted" class="text-muted-foreground text-sm">
|
|
||||||
Pulang: {{ todayAttendance.check_out_at_formatted }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="todayAttendance?.work_duration_formatted" class="text-muted-foreground text-sm">
|
|
||||||
Durasi: {{ todayAttendance.work_duration_formatted }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<Button :disabled="!canCheckInToday" @click="openCheckInModal">
|
|
||||||
<LogIn class="size-4" />
|
|
||||||
Presensi Masuk
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button variant="outline" :disabled="!canCheckOutToday" @click="openCheckOutModal">
|
|
||||||
<LogOut class="size-4" />
|
|
||||||
Presensi Pulang
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card class="min-w-0 overflow-hidden">
|
<Card class="min-w-0 overflow-hidden">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user