feat: enhance user profile and employee management with profile photo handling and validation updates
This commit is contained in:
parent
5cffe43a07
commit
f6b5a78f4b
@ -7,6 +7,7 @@
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Account\UpdateProfileRequest;
|
use App\Http\Requests\Admin\Account\UpdateProfileRequest;
|
||||||
use App\Services\Account\ProfileService;
|
use App\Services\Account\ProfileService;
|
||||||
|
use App\Support\Media\MediaPresenter;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@ -26,6 +27,9 @@ public function edit(): Response
|
|||||||
return Inertia::render('admin/account/Profile', [
|
return Inertia::render('admin/account/Profile', [
|
||||||
'genders' => Gender::selectOptions(),
|
'genders' => Gender::selectOptions(),
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
|
'profilePhoto' => $user->profile
|
||||||
|
? MediaPresenter::first($user->profile, 'profile_photo')
|
||||||
|
: null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
use App\Http\Requests\Admin\ToggleStatusRequest;
|
use App\Http\Requests\Admin\ToggleStatusRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Hr\EmployeeService;
|
use App\Services\Hr\EmployeeService;
|
||||||
|
use App\Support\Media\MediaPresenter;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@ -85,6 +86,9 @@ public function edit(User $user): Response
|
|||||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||||
'roles' => $this->assignableRoleOptions(),
|
'roles' => $this->assignableRoleOptions(),
|
||||||
'employee' => $user,
|
'employee' => $user,
|
||||||
|
'profilePhoto' => $user->profile
|
||||||
|
? MediaPresenter::first($user->profile, 'profile_photo')
|
||||||
|
: null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,7 @@ public function share(Request $request): array
|
|||||||
'last_login_at' => $request->user()->last_login_at,
|
'last_login_at' => $request->user()->last_login_at,
|
||||||
'roles' => $request->user()->getRoleNames(),
|
'roles' => $request->user()->getRoleNames(),
|
||||||
'permissions' => $request->user()->getAllPermissions()->pluck('name'),
|
'permissions' => $request->user()->getAllPermissions()->pluck('name'),
|
||||||
|
'profile_photo_url' => $request->user()->profile?->profile_photo_url,
|
||||||
] : null,
|
] : null,
|
||||||
'password_default' => config('auth.password_default'),
|
'password_default' => config('auth.password_default'),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -27,6 +27,9 @@ public function rules(): array
|
|||||||
'gender' => ['nullable', Rule::enum(Gender::class)],
|
'gender' => ['nullable', Rule::enum(Gender::class)],
|
||||||
'birth_date' => ['nullable', 'date', 'before:today'],
|
'birth_date' => ['nullable', 'date', 'before:today'],
|
||||||
'address' => ['nullable', 'string'],
|
'address' => ['nullable', 'string'],
|
||||||
|
'profile_photo' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
|
||||||
|
'remove_profile_photo_ids' => ['nullable', 'array'],
|
||||||
|
'remove_profile_photo_ids.*' => ['integer'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +46,7 @@ public function attributes(): array
|
|||||||
'gender' => 'jenis kelamin',
|
'gender' => 'jenis kelamin',
|
||||||
'birth_date' => 'tanggal lahir',
|
'birth_date' => 'tanggal lahir',
|
||||||
'address' => 'alamat',
|
'address' => 'alamat',
|
||||||
|
'profile_photo' => 'foto profil',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,9 @@ public function rules(): array
|
|||||||
'birth_date' => ['nullable', 'date', 'before:today'],
|
'birth_date' => ['nullable', 'date', 'before:today'],
|
||||||
'address' => ['nullable', 'string'],
|
'address' => ['nullable', 'string'],
|
||||||
'role' => ['required', Rule::in(Role::assignableValues())],
|
'role' => ['required', Rule::in(Role::assignableValues())],
|
||||||
|
'profile_photo' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
|
||||||
|
'remove_profile_photo_ids' => ['nullable', 'array'],
|
||||||
|
'remove_profile_photo_ids.*' => ['integer'],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (! $isOwner) {
|
if (! $isOwner) {
|
||||||
@ -61,6 +64,7 @@ public function attributes(): array
|
|||||||
'gender' => 'jenis kelamin',
|
'gender' => 'jenis kelamin',
|
||||||
'birth_date' => 'tanggal lahir',
|
'birth_date' => 'tanggal lahir',
|
||||||
'address' => 'alamat',
|
'address' => 'alamat',
|
||||||
|
'profile_photo' => 'foto profil',
|
||||||
'role' => 'role',
|
'role' => 'role',
|
||||||
'join_date' => 'tanggal bergabung',
|
'join_date' => 'tanggal bergabung',
|
||||||
'employment_status' => 'status kepegawaian',
|
'employment_status' => 'status kepegawaian',
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\OwnerVerificationStatus;
|
use App\Enums\OwnerVerificationStatus;
|
||||||
use App\Models\Concerns\HasPendingOwnerVerification;
|
|
||||||
use App\Models\Concerns\HasModuleMedia;
|
use App\Models\Concerns\HasModuleMedia;
|
||||||
|
use App\Models\Concerns\HasPendingOwnerVerification;
|
||||||
use App\Models\Concerns\InteractsWithActivityLog;
|
use App\Models\Concerns\InteractsWithActivityLog;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\Gender;
|
use App\Enums\Gender;
|
||||||
|
use App\Models\Concerns\HasModuleMedia;
|
||||||
use App\Models\Concerns\InteractsWithActivityLog;
|
use App\Models\Concerns\InteractsWithActivityLog;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||||
@ -12,12 +13,23 @@
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
|
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
#[Appends(['birth_date_formatted', 'birth_date_input', 'gender_label'])]
|
#[Appends(['birth_date_formatted', 'birth_date_input', 'gender_label', 'profile_photo_url'])]
|
||||||
class UserProfile extends Model
|
class UserProfile extends Model implements HasMedia
|
||||||
{
|
{
|
||||||
use HasFactory, InteractsWithActivityLog, SoftDeletes;
|
use HasFactory, HasModuleMedia, InteractsWithActivityLog, SoftDeletes;
|
||||||
|
|
||||||
|
public static function mediaModuleName(): string
|
||||||
|
{
|
||||||
|
return 'user-profile';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerMediaCollections(): void
|
||||||
|
{
|
||||||
|
$this->addMediaCollection('profile_photo')->singleFile();
|
||||||
|
}
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
@ -52,4 +64,11 @@ public function genderLabel(): Attribute
|
|||||||
get: fn () => $this->gender?->label(),
|
get: fn () => $this->gender?->label(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function profilePhotoUrl(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => $this->getFirstMediaUrl('profile_photo') ?: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
namespace App\Services\Account;
|
namespace App\Services\Account;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\UserProfile;
|
||||||
|
use App\Services\Media\MediaService;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@ -10,6 +12,10 @@
|
|||||||
|
|
||||||
class ProfileService
|
class ProfileService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MediaService $mediaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $validated
|
* @param array<string, mixed> $validated
|
||||||
*/
|
*/
|
||||||
@ -22,7 +28,8 @@ public function update(array $validated, User $user): void
|
|||||||
'username' => $validated['username'],
|
'username' => $validated['username'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user->profile()->updateOrCreate(
|
/** @var UserProfile $profile */
|
||||||
|
$profile = $user->profile()->updateOrCreate(
|
||||||
['user_id' => $user->id],
|
['user_id' => $user->id],
|
||||||
[
|
[
|
||||||
'full_name' => $validated['full_name'],
|
'full_name' => $validated['full_name'],
|
||||||
@ -32,6 +39,11 @@ public function update(array $validated, User $user): void
|
|||||||
'address' => $validated['address'] ?? null,
|
'address' => $validated['address'] ?? null,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$newFiles = ! empty($validated['profile_photo']) ? [$validated['profile_photo']] : [];
|
||||||
|
$removeIds = $validated['remove_profile_photo_ids'] ?? [];
|
||||||
|
|
||||||
|
$this->mediaService->syncCollection($profile, 'profile_photo', $newFiles, $removeIds, 1);
|
||||||
});
|
});
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
throw $e;
|
throw $e;
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\Employee;
|
use App\Models\Employee;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserProfile;
|
use App\Models\UserProfile;
|
||||||
|
use App\Services\Media\MediaService;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@ -15,6 +16,10 @@
|
|||||||
|
|
||||||
class EmployeeService
|
class EmployeeService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MediaService $mediaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||||
*/
|
*/
|
||||||
@ -66,7 +71,7 @@ public function create(array $validated): void
|
|||||||
'password' => Hash::make(config('auth.password_default')),
|
'password' => Hash::make(config('auth.password_default')),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
UserProfile::create([
|
$profile = UserProfile::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'full_name' => $validated['full_name'],
|
'full_name' => $validated['full_name'],
|
||||||
'phone_number' => $validated['phone_number'],
|
'phone_number' => $validated['phone_number'],
|
||||||
@ -75,6 +80,8 @@ public function create(array $validated): void
|
|||||||
'address' => $validated['address'],
|
'address' => $validated['address'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->syncProfilePhoto($profile, $validated);
|
||||||
|
|
||||||
if ($validated['role'] !== Role::OWNER->value) {
|
if ($validated['role'] !== Role::OWNER->value) {
|
||||||
Employee::create([
|
Employee::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
@ -113,7 +120,8 @@ public function update(User $user, array $validated): void
|
|||||||
'username' => $validated['username'],
|
'username' => $validated['username'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user->profile()->updateOrCreate(
|
/** @var UserProfile $profile */
|
||||||
|
$profile = $user->profile()->updateOrCreate(
|
||||||
['user_id' => $user->id],
|
['user_id' => $user->id],
|
||||||
[
|
[
|
||||||
'full_name' => $validated['full_name'],
|
'full_name' => $validated['full_name'],
|
||||||
@ -124,6 +132,8 @@ public function update(User $user, array $validated): void
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->syncProfilePhoto($profile, $validated);
|
||||||
|
|
||||||
$hasEmployee = ! empty($validated['join_date']) && ! empty($validated['employment_status']) && ! empty($validated['base_salary']);
|
$hasEmployee = ! empty($validated['join_date']) && ! empty($validated['employment_status']) && ! empty($validated['base_salary']);
|
||||||
|
|
||||||
if ($hasEmployee) {
|
if ($hasEmployee) {
|
||||||
@ -203,6 +213,17 @@ public function delete(User $user): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $validated
|
||||||
|
*/
|
||||||
|
private function syncProfilePhoto(UserProfile $profile, array $validated): void
|
||||||
|
{
|
||||||
|
$newFiles = ! empty($validated['profile_photo']) ? [$validated['profile_photo']] : [];
|
||||||
|
$removeIds = $validated['remove_profile_photo_ids'] ?? [];
|
||||||
|
|
||||||
|
$this->mediaService->syncCollection($profile, 'profile_photo', $newFiles, $removeIds, 1);
|
||||||
|
}
|
||||||
|
|
||||||
private function applySorting(Builder $query, string $sort, string $direction): void
|
private function applySorting(Builder $query, string $sort, string $direction): void
|
||||||
{
|
{
|
||||||
$employeeSorts = [
|
$employeeSorts = [
|
||||||
|
|||||||
@ -11,7 +11,6 @@
|
|||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
use App\Models\RawMaterial;
|
use App\Models\RawMaterial;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Manage\PurchaseService;
|
|
||||||
use App\Services\Master\ProductService;
|
use App\Services\Master\ProductService;
|
||||||
use App\Services\Master\RawMaterialService;
|
use App\Services\Master\RawMaterialService;
|
||||||
use App\Services\System\PushNotificationService;
|
use App\Services\System\PushNotificationService;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -37,6 +37,7 @@ function logout() {
|
|||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<Button variant="ghost" class="relative size-8 rounded-full">
|
<Button variant="ghost" class="relative size-8 rounded-full">
|
||||||
<Avatar class="size-8">
|
<Avatar class="size-8">
|
||||||
|
<AvatarImage v-if="user.profile_photo_url" :src="user.profile_photo_url" :alt="user.username" />
|
||||||
<AvatarFallback>{{ initials }}</AvatarFallback>
|
<AvatarFallback>{{ initials }}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, useForm } from '@inertiajs/vue3';
|
import { Head, useForm } from '@inertiajs/vue3';
|
||||||
import { Save } from '@lucide/vue';
|
import { Save } from '@lucide/vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { PhoneNumberInput } from '@/components/form/phone-number-input';
|
import { PhoneNumberInput } from '@/components/form/phone-number-input';
|
||||||
|
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { DatePicker } from '@/components/ui/date-picker';
|
import { DatePicker } from '@/components/ui/date-picker';
|
||||||
@ -25,11 +27,14 @@ import type {
|
|||||||
ProfileFormData,
|
ProfileFormData,
|
||||||
ProfileUser,
|
ProfileUser,
|
||||||
} from '@/types/account';
|
} from '@/types/account';
|
||||||
|
import { createMediaUploadState } from '@/types/media';
|
||||||
|
import type { MediaItem, MediaUploadState } from '@/types/media';
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: ProfileUser;
|
user: ProfileUser;
|
||||||
genders: EnumOption[];
|
genders: EnumOption[];
|
||||||
|
profilePhoto?: MediaItem | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const form = useForm<ProfileFormData>({
|
const form = useForm<ProfileFormData>({
|
||||||
@ -42,8 +47,36 @@ const form = useForm<ProfileFormData>({
|
|||||||
address: props.user.profile?.address ?? '',
|
address: props.user.profile?.address ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const profilePhotoState = ref<MediaUploadState>(
|
||||||
|
createMediaUploadState(props.profilePhoto ? [props.profilePhoto] : []),
|
||||||
|
);
|
||||||
|
|
||||||
|
function buildFormData(): FormData {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('email', form.email);
|
||||||
|
formData.append('username', form.username);
|
||||||
|
formData.append('full_name', form.full_name);
|
||||||
|
formData.append('phone_number', form.phone_number);
|
||||||
|
formData.append('gender', form.gender);
|
||||||
|
formData.append('birth_date', form.birth_date);
|
||||||
|
formData.append('address', form.address);
|
||||||
|
|
||||||
|
profilePhotoState.value.newFiles.forEach((file) => {
|
||||||
|
formData.append('profile_photo', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
profilePhotoState.value.removeIds.forEach((id) => {
|
||||||
|
formData.append('remove_profile_photo_ids[]', String(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
form.put(update.url(), {
|
const payload = buildFormData();
|
||||||
|
|
||||||
|
form.transform(() => payload).put(update.url(), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: (errors: any) => {
|
onError: (errors: any) => {
|
||||||
if (errors.system) {
|
if (errors.system) {
|
||||||
@ -124,6 +157,10 @@ function submit() {
|
|||||||
<FieldError :errors="formErrors(form, 'gender')" />
|
<FieldError :errors="formErrors(form, 'gender')" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<MediaDropzone id="profile_photo" v-model="profilePhotoState" label="Foto Profil"
|
||||||
|
description="Opsional. Format: JPG, JPEG, PNG, atau WebP. Maks. 2 MB."
|
||||||
|
:max-files="1" :errors="formErrors(form, 'profile_photo')" class="sm:col-span-3" />
|
||||||
|
|
||||||
<Field class="sm:col-span-3">
|
<Field class="sm:col-span-3">
|
||||||
<FieldLabel for="address">
|
<FieldLabel for="address">
|
||||||
Alamat
|
Alamat
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { EmployeeListItem, EnumOption } from '@/types/employee';
|
import type { EmployeeListItem, EnumOption } from '@/types/employee';
|
||||||
|
import type { MediaItem } from '@/types/media';
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import EmployeeForm from './form/EmployeeForm.vue';
|
import EmployeeForm from './form/EmployeeForm.vue';
|
||||||
@ -12,6 +13,7 @@ const props = defineProps<{
|
|||||||
genders: EnumOption[];
|
genders: EnumOption[];
|
||||||
employmentStatuses: EnumOption[];
|
employmentStatuses: EnumOption[];
|
||||||
roles: EnumOption[];
|
roles: EnumOption[];
|
||||||
|
profilePhoto?: MediaItem | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const initialData = computed(() => ({
|
const initialData = computed(() => ({
|
||||||
@ -51,6 +53,7 @@ const initialData = computed(() => ({
|
|||||||
method="put"
|
method="put"
|
||||||
submit-label="Perbarui"
|
submit-label="Perbarui"
|
||||||
:initial-data="initialData"
|
:initial-data="initialData"
|
||||||
|
:profile-photo="profilePhoto"
|
||||||
:employee-id="employee.id"
|
:employee-id="employee.id"
|
||||||
:employee-name="employee.profile?.full_name"
|
:employee-name="employee.profile?.full_name"
|
||||||
:genders="genders"
|
:genders="genders"
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useForm } from '@inertiajs/vue3';
|
import { useForm } from '@inertiajs/vue3';
|
||||||
import { Save } from '@lucide/vue';
|
import { Save } from '@lucide/vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { PhoneNumberInput } from '@/components/form/phone-number-input';
|
import { PhoneNumberInput } from '@/components/form/phone-number-input';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
|
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { DatePicker } from '@/components/ui/date-picker';
|
import { DatePicker } from '@/components/ui/date-picker';
|
||||||
@ -28,6 +30,8 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import type { EmployeeFormData, EnumOption } from '@/types/employee';
|
import type { EmployeeFormData, EnumOption } from '@/types/employee';
|
||||||
|
import { createMediaUploadState } from '@/types/media';
|
||||||
|
import type { MediaItem, MediaUploadState } from '@/types/media';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -35,6 +39,7 @@ const props = withDefaults(
|
|||||||
employmentStatuses: EnumOption[];
|
employmentStatuses: EnumOption[];
|
||||||
roles: EnumOption[];
|
roles: EnumOption[];
|
||||||
initialData?: Partial<EmployeeFormData>;
|
initialData?: Partial<EmployeeFormData>;
|
||||||
|
profilePhoto?: MediaItem | null;
|
||||||
submitUrl: string;
|
submitUrl: string;
|
||||||
method?: 'post' | 'put';
|
method?: 'post' | 'put';
|
||||||
submitLabel?: string;
|
submitLabel?: string;
|
||||||
@ -61,13 +66,38 @@ const form = useForm<EmployeeFormData>({
|
|||||||
role: props.initialData?.role ?? '',
|
role: props.initialData?.role ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const profilePhotoState = ref<MediaUploadState>(
|
||||||
|
createMediaUploadState(props.profilePhoto ? [props.profilePhoto] : []),
|
||||||
|
);
|
||||||
|
|
||||||
|
function buildFormData(): FormData {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('email', form.email);
|
||||||
|
formData.append('username', form.username);
|
||||||
|
formData.append('full_name', form.full_name);
|
||||||
|
formData.append('phone_number', form.phone_number);
|
||||||
|
formData.append('gender', form.gender);
|
||||||
|
formData.append('birth_date', form.birth_date);
|
||||||
|
formData.append('address', form.address);
|
||||||
|
formData.append('join_date', form.join_date === '' ? '' : form.join_date);
|
||||||
|
formData.append('employment_status', form.employment_status === 'none' || form.employment_status === '' ? '' : form.employment_status);
|
||||||
|
formData.append('base_salary', form.base_salary === '' ? '' : form.base_salary);
|
||||||
|
formData.append('role', form.role);
|
||||||
|
|
||||||
|
profilePhotoState.value.newFiles.forEach((file) => {
|
||||||
|
formData.append('profile_photo', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
profilePhotoState.value.removeIds.forEach((id) => {
|
||||||
|
formData.append('remove_profile_photo_ids[]', String(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
form.transform((data) => ({
|
const payload = buildFormData();
|
||||||
...data,
|
|
||||||
join_date: data.join_date === '' ? null : data.join_date,
|
|
||||||
employment_status: data.employment_status === 'none' || data.employment_status === '' ? null : data.employment_status,
|
|
||||||
base_salary: data.base_salary === '' ? null : data.base_salary,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
onError: (errors: any) => {
|
onError: (errors: any) => {
|
||||||
@ -78,9 +108,9 @@ function submit() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (props.method === 'put') {
|
if (props.method === 'put') {
|
||||||
form.put(props.submitUrl, options);
|
form.transform(() => payload).put(props.submitUrl, options);
|
||||||
} else {
|
} else {
|
||||||
form.post(props.submitUrl, options);
|
form.transform(() => payload).post(props.submitUrl, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -164,6 +194,9 @@ function submit() {
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<FieldError :errors="formErrors(form, 'gender')" />
|
<FieldError :errors="formErrors(form, 'gender')" />
|
||||||
</Field>
|
</Field>
|
||||||
|
<MediaDropzone id="profile_photo" v-model="profilePhotoState" label="Foto Profil"
|
||||||
|
description="Opsional. Format: JPG, JPEG, PNG, atau WebP. Maks. 2 MB." :max-files="1"
|
||||||
|
:errors="formErrors(form, 'profile_photo')" class="md:col-span-3" />
|
||||||
<Field class="md:col-span-3">
|
<Field class="md:col-span-3">
|
||||||
<FieldLabel for="address">Alamat</FieldLabel>
|
<FieldLabel for="address">Alamat</FieldLabel>
|
||||||
<Textarea id="address" v-model="form.address"
|
<Textarea id="address" v-model="form.address"
|
||||||
|
|||||||
@ -1,12 +1,27 @@
|
|||||||
import type { ColumnDef } from '@tanstack/vue-table';
|
import type { ColumnDef } from '@tanstack/vue-table';
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { DataTableColumnHeader } from '@/components/data-table';
|
import { DataTableColumnHeader } from '@/components/data-table';
|
||||||
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import type { EmployeeListItem } from '@/types/employee';
|
import type { EmployeeListItem } from '@/types/employee';
|
||||||
|
import type { MediaItem } from '@/types/media';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
import DataTableActions from './data-table-actions.vue';
|
||||||
import EmployeeStatusToggle from './employee-status-toggle.vue';
|
import EmployeeStatusToggle from './employee-status-toggle.vue';
|
||||||
|
|
||||||
export const columns: ColumnDef<EmployeeListItem>[] = [
|
export const columns: ColumnDef<EmployeeListItem>[] = [
|
||||||
|
{
|
||||||
|
id: 'photo',
|
||||||
|
enableSorting: false,
|
||||||
|
header: () => 'Foto',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const photoUrl = row.original.profile?.profile_photo_url;
|
||||||
|
const items: MediaItem[] = photoUrl
|
||||||
|
? [{ id: 0, url: photoUrl, thumb_url: photoUrl }]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return h(MediaThumbnailCell, { items, maxVisible: 1 });
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'profile.full_name',
|
accessorKey: 'profile.full_name',
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export type UserProfile = {
|
|||||||
gender_label: string | null;
|
gender_label: string | null;
|
||||||
birth_date_input: string | null;
|
birth_date_input: string | null;
|
||||||
address: string | null;
|
address: string | null;
|
||||||
|
profile_photo_url: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProfileUser = {
|
export type ProfileUser = {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export type EmployeeProfile = {
|
|||||||
birth_date_formatted: string | null;
|
birth_date_formatted: string | null;
|
||||||
birth_date_input: string | null;
|
birth_date_input: string | null;
|
||||||
address: string | null;
|
address: string | null;
|
||||||
|
profile_photo_url: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmployeeRecord = {
|
export type EmployeeRecord = {
|
||||||
|
|||||||
@ -9,8 +9,6 @@
|
|||||||
use App\Models\OwnerVerificationRequest;
|
use App\Models\OwnerVerificationRequest;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
use App\Models\RawMaterialPrice;
|
|
||||||
use App\Models\Supplier;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Support\ActivityLog\ModelLabel;
|
use App\Support\ActivityLog\ModelLabel;
|
||||||
use Database\Seeders\RolePermissionSeeder;
|
use Database\Seeders\RolePermissionSeeder;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user