refactor: enhance error handling in form submissions across various Vue components to improve user feedback and maintainability
This commit is contained in:
parent
58ba7512a5
commit
6219f5032d
@ -33,13 +33,10 @@ const form = useForm({
|
|||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
form.post('/auth/login', {
|
form.post('/auth/login', {
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message =
|
if (errors.system) {
|
||||||
errors.login ??
|
toast.error(errors.system);
|
||||||
errors.password ??
|
}
|
||||||
'Gagal masuk. Periksa kembali data Anda.';
|
|
||||||
|
|
||||||
toast.error(message);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,8 +66,10 @@ function onAppearanceChange(value: AcceptableValue) {
|
|||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
form.put(update.url(), {
|
form.put(update.url(), {
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan tampilan.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,9 +15,9 @@ import {
|
|||||||
import { InputGroup, InputGroupInput, InputGroupAddon, InputGroupButton } from '@/components/ui/input-group';
|
import { InputGroup, InputGroupInput, InputGroupAddon, InputGroupButton } from '@/components/ui/input-group';
|
||||||
import AccountLayout from '@/layouts/AccountLayout.vue';
|
import AccountLayout from '@/layouts/AccountLayout.vue';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
|
import { update } from '@/routes/admin/account/password';
|
||||||
import type { PasswordFormData } from '@/types/account';
|
import type { PasswordFormData } from '@/types/account';
|
||||||
|
|
||||||
import { update } from '@/routes/admin/account/password';
|
|
||||||
|
|
||||||
const form = useForm<PasswordFormData>({
|
const form = useForm<PasswordFormData>({
|
||||||
current_password: '',
|
current_password: '',
|
||||||
@ -34,8 +34,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal memperbarui kata sandi. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,13 +19,13 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import AccountLayout from '@/layouts/AccountLayout.vue';
|
import AccountLayout from '@/layouts/AccountLayout.vue';
|
||||||
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 { update } from '@/routes/admin/account/profile';
|
||||||
import type {
|
import type {
|
||||||
EnumOption,
|
EnumOption,
|
||||||
ProfileFormData,
|
ProfileFormData,
|
||||||
ProfileUser,
|
ProfileUser,
|
||||||
} from '@/types/account';
|
} from '@/types/account';
|
||||||
|
|
||||||
import { update } from '@/routes/admin/account/profile';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: ProfileUser;
|
user: ProfileUser;
|
||||||
@ -45,8 +45,10 @@ const form = useForm<ProfileFormData>({
|
|||||||
function submit() {
|
function submit() {
|
||||||
form.put(update.url(), {
|
form.put(update.url(), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan profil. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -76,8 +78,8 @@ function submit() {
|
|||||||
<FieldLabel for="username" required>
|
<FieldLabel for="username" required>
|
||||||
Username
|
Username
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<Input id="username" v-model="form.username" type="text" placeholder="Contoh: johndoe"
|
<Input id="username" v-model="form.username" type="text"
|
||||||
:maxlength="FIELD_LIMITS.username" />
|
placeholder="Contoh: johndoe" :maxlength="FIELD_LIMITS.username" />
|
||||||
<FieldError :errors="formErrors(form, 'username')" />
|
<FieldError :errors="formErrors(form, 'username')" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@ -126,8 +128,8 @@ function submit() {
|
|||||||
<FieldLabel for="address">
|
<FieldLabel for="address">
|
||||||
Alamat
|
Alamat
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<Textarea id="address" v-model="form.address" placeholder="Contoh: Jl. Sudirman No. 123, Jakarta"
|
<Textarea id="address" v-model="form.address"
|
||||||
rows="3" />
|
placeholder="Contoh: Jl. Sudirman No. 123, Jakarta" rows="3" />
|
||||||
<FieldError :errors="formErrors(form, 'address')" />
|
<FieldError :errors="formErrors(form, 'address')" />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@ -26,11 +26,11 @@ import { CashTransactionType } from '@/constants/cash-transaction-type';
|
|||||||
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 { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
|
import { withdraw, deposit } from '@/routes/admin/finance/cash';
|
||||||
|
import { update } from '@/routes/admin/finance/cash/transactions';
|
||||||
import type { CashTransactionListItem } from '@/types/cash';
|
import type { CashTransactionListItem } from '@/types/cash';
|
||||||
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
|
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
|
||||||
import type { MediaUploadState } from '@/types/media';
|
import type { MediaUploadState } from '@/types/media';
|
||||||
import { withdraw, deposit } from '@/routes/admin/finance/cash';
|
|
||||||
import { update } from '@/routes/admin/finance/cash/transactions';
|
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -105,10 +105,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error(isEditing.value
|
if (errors.system) {
|
||||||
? 'Gagal memperbarui transaksi kas. Periksa kembali formulir.'
|
toast.error(errors.system);
|
||||||
: 'Gagal mencatat transaksi kas. Periksa kembali formulir.');
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,8 @@ import { useFormDialog } from '@/composables/useFormDialog';
|
|||||||
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 { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
import type { EmployeeAdvanceFormData, EmployeeAdvanceListItem } from '@/types/employee-advance';
|
|
||||||
import { store, update } from '@/routes/admin/finance/employee_advances';
|
import { store, update } from '@/routes/admin/finance/employee_advances';
|
||||||
|
import type { EmployeeAdvanceFormData, EmployeeAdvanceListItem } from '@/types/employee-advance';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -89,8 +89,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -21,8 +21,8 @@ import {
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
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 { EmployeeAdvanceListItem } from '@/types/employee-advance';
|
|
||||||
import { reject } from '@/routes/admin/finance/employee_advances';
|
import { reject } from '@/routes/admin/finance/employee_advances';
|
||||||
|
import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -55,8 +55,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menolak kasbon. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { toast } from 'vue-sonner';
|
|||||||
import { RowApproveAction, RowDeleteAction, RowEditAction, RowPayAction, RowRejectAction } from '@/components/button';
|
import { RowApproveAction, RowDeleteAction, RowEditAction, RowPayAction, RowRejectAction } from '@/components/button';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
|
|
||||||
import { destroy, approve, pay } from '@/routes/admin/finance/employee_advances';
|
import { destroy, approve, pay } from '@/routes/admin/finance/employee_advances';
|
||||||
|
import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
employeeAdvance: EmployeeAdvanceListItem;
|
employeeAdvance: EmployeeAdvanceListItem;
|
||||||
@ -52,8 +52,10 @@ function approveEmployeeAdvance() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
approveConfirmOpen.value = false;
|
approveConfirmOpen.value = false;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
toast.error(errors.amount || errors.employee_advance || 'Gagal menyetujui kasbon.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
approveProcessing.value = false;
|
approveProcessing.value = false;
|
||||||
@ -69,8 +71,10 @@ function payEmployeeAdvance() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
payConfirmOpen.value = false;
|
payConfirmOpen.value = false;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
toast.error(errors.amount || errors.employee_advance || 'Gagal melunasi kasbon.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
payProcessing.value = false;
|
payProcessing.value = false;
|
||||||
|
|||||||
@ -25,11 +25,11 @@ import { useFormDialog } from '@/composables/useFormDialog';
|
|||||||
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 { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
|
import { store, update } from '@/routes/admin/finance/expenses';
|
||||||
import type { ExpenseListItem } from '@/types/expense';
|
import type { ExpenseListItem } from '@/types/expense';
|
||||||
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
|
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
|
||||||
import type { MediaUploadState } from '@/types/media';
|
import type { MediaUploadState } from '@/types/media';
|
||||||
|
|
||||||
import { store, update } from '@/routes/admin/finance/expenses';
|
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -95,8 +95,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { Save } from '@lucide/vue';
|
|||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@ -26,8 +25,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 { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
import type { PayrollAdjustmentFormData, PayrollAdjustmentItem, PayrollListItem, SelectOption } from '@/types/payroll';
|
|
||||||
import { store, update, destroy } from '@/routes/admin/finance/payroll/adjustments';
|
import { store, update, destroy } from '@/routes/admin/finance/payroll/adjustments';
|
||||||
|
import type { PayrollAdjustmentFormData, PayrollAdjustmentItem, PayrollListItem, SelectOption } from '@/types/payroll';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -86,8 +85,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
cancelEdit();
|
cancelEdit();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal memperbarui penyesuaian.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -96,8 +97,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
resetForm();
|
resetForm();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menambahkan penyesuaian.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -136,7 +139,8 @@ function submit() {
|
|||||||
tooltip-class="z-210" @click="startEdit(adj)" />
|
tooltip-class="z-210" @click="startEdit(adj)" />
|
||||||
<RowDeleteAction :action-url="destroy.url(adj.id)" tooltip="Hapus"
|
<RowDeleteAction :action-url="destroy.url(adj.id)" tooltip="Hapus"
|
||||||
button-class="hover:bg-destructive/10" tooltip-class="z-210"
|
button-class="hover:bg-destructive/10" tooltip-class="z-210"
|
||||||
title="Hapus penyesuaian?" :description="`Penyesuaian ${adj.type_label} ${adj.amount_formatted} akan dihapus.`"
|
title="Hapus penyesuaian?"
|
||||||
|
:description="`Penyesuaian ${adj.type_label} ${adj.amount_formatted} akan dihapus.`"
|
||||||
error-message="Gagal menghapus penyesuaian."
|
error-message="Gagal menghapus penyesuaian."
|
||||||
:on-success="() => toast.success('Penyesuaian berhasil dihapus.')" />
|
:on-success="() => toast.success('Penyesuaian berhasil dihapus.')" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useForm } from '@inertiajs/vue3';
|
||||||
|
import { Camera, Loader2 } from '@lucide/vue';
|
||||||
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -9,12 +13,8 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { useGeolocation } from '@/composables/useGeolocation';
|
import { useGeolocation } from '@/composables/useGeolocation';
|
||||||
import { useWebcamCapture } from '@/composables/useWebcamCapture';
|
import { useWebcamCapture } from '@/composables/useWebcamCapture';
|
||||||
import type { AttendanceCaptureFormData } from '@/types/attendance';
|
|
||||||
import { useForm } from '@inertiajs/vue3';
|
|
||||||
import { Camera, Loader2 } from '@lucide/vue';
|
|
||||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
|
||||||
import { toast } from 'vue-sonner';
|
|
||||||
import { check_in, check_out } from '@/routes/admin/hr/attendances';
|
import { check_in, check_out } from '@/routes/admin/hr/attendances';
|
||||||
|
import type { AttendanceCaptureFormData } from '@/types/attendance';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -124,8 +124,10 @@ function submitAttendance() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan presensi.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -162,69 +164,37 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<div v-if="!previewPhoto" class="space-y-2">
|
<div v-if="!previewPhoto" class="space-y-2">
|
||||||
<p class="text-sm font-medium">Kamera</p>
|
<p class="text-sm font-medium">Kamera</p>
|
||||||
<div
|
<div class="relative aspect-4/3 overflow-hidden rounded-lg border bg-muted">
|
||||||
class="relative aspect-4/3 overflow-hidden rounded-lg border bg-muted"
|
<video ref="videoRef" class="size-full scale-x-[-1] object-cover" autoplay muted playsinline />
|
||||||
>
|
|
||||||
<video
|
|
||||||
ref="videoRef"
|
|
||||||
class="size-full scale-x-[-1] object-cover"
|
|
||||||
autoplay
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div v-if="cameraLoading"
|
||||||
v-if="cameraLoading"
|
class="absolute inset-0 flex items-center justify-center bg-background/80">
|
||||||
class="absolute inset-0 flex items-center justify-center bg-background/80"
|
<Loader2 class="size-8 animate-spin text-muted-foreground" />
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
class="size-8 animate-spin text-muted-foreground"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-2">
|
<div v-else class="space-y-2">
|
||||||
<p class="text-sm font-medium">Hasil Foto</p>
|
<p class="text-sm font-medium">Hasil Foto</p>
|
||||||
<div
|
<div class="relative aspect-4/3 overflow-hidden rounded-lg border bg-muted">
|
||||||
class="relative aspect-4/3 overflow-hidden rounded-lg border bg-muted"
|
|
||||||
>
|
|
||||||
<img :src="previewPhoto" alt="Hasil foto presensi" class="size-full object-cover" />
|
<img :src="previewPhoto" alt="Hasil foto presensi" class="size-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button v-if="previewPhoto" type="button" variant="outline" @click="retakePhoto">
|
||||||
v-if="previewPhoto"
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
@click="retakePhoto"
|
|
||||||
>
|
|
||||||
Ambil Ulang
|
Ambil Ulang
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button v-if="!previewPhoto" type="button" :disabled="!cameraReady || capturing" @click="capturePhoto">
|
||||||
v-if="!previewPhoto"
|
|
||||||
type="button"
|
|
||||||
:disabled="!cameraReady || capturing"
|
|
||||||
@click="capturePhoto"
|
|
||||||
>
|
|
||||||
<Loader2 v-if="capturing" class="size-4 animate-spin" />
|
<Loader2 v-if="capturing" class="size-4 animate-spin" />
|
||||||
<Camera v-else class="size-4" />
|
<Camera v-else class="size-4" />
|
||||||
Ambil Foto
|
Ambil Foto
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button v-else type="button" :disabled="form.processing" @click="submitAttendance">
|
||||||
v-else
|
<Loader2 v-if="form.processing" class="size-4 animate-spin" />
|
||||||
type="button"
|
|
||||||
:disabled="form.processing"
|
|
||||||
@click="submitAttendance"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="form.processing"
|
|
||||||
class="size-4 animate-spin"
|
|
||||||
/>
|
|
||||||
{{ submitLabel }}
|
{{ submitLabel }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -70,8 +70,10 @@ function submit() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { router, usePage } from '@inertiajs/vue3';
|
import { router, usePage } from '@inertiajs/vue3';
|
||||||
import { computed, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@ -35,8 +35,10 @@ function resetPassword() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal mereset kata sandi.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
processing.value = false;
|
processing.value = false;
|
||||||
@ -58,7 +60,9 @@ function resetPassword() {
|
|||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Kata sandi
|
Kata sandi
|
||||||
<span class="text-foreground font-medium">{{ employeeName ?? 'pegawai' }}</span>
|
<span class="text-foreground font-medium">{{ employeeName ?? 'pegawai' }}</span>
|
||||||
akan direset ke kata sandi default sistem (<span class="font-mono bg-muted px-1.5 py-0.5 rounded text-foreground font-medium">{{ defaultPassword }}</span>). Pengguna akan otomatis logout dari
|
akan direset ke kata sandi default sistem (<span
|
||||||
|
class="font-mono bg-muted px-1.5 py-0.5 rounded text-foreground font-medium">{{ defaultPassword
|
||||||
|
}}</span>). Pengguna akan otomatis logout dari
|
||||||
semua sesi aktif.
|
semua sesi aktif.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
@ -66,10 +70,7 @@ function resetPassword() {
|
|||||||
<AlertDialogCancel :disabled="processing">
|
<AlertDialogCancel :disabled="processing">
|
||||||
Batal
|
Batal
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction :disabled="processing" @click.prevent="resetPassword">
|
||||||
:disabled="processing"
|
|
||||||
@click.prevent="resetPassword"
|
|
||||||
>
|
|
||||||
{{ processing ? 'Memproses...' : 'Reset Kata Sandi' }}
|
{{ processing ? 'Memproses...' : 'Reset Kata Sandi' }}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import { toast } from 'vue-sonner';
|
|||||||
import { RowDeleteAction, RowEditAction, RowStatusAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction, RowStatusAction } from '@/components/button';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { EmployeeListItem } from '@/types/employee';
|
|
||||||
import { edit, destroy, reset_password } from '@/routes/admin/hr/employees';
|
import { edit, destroy, reset_password } from '@/routes/admin/hr/employees';
|
||||||
|
import type { EmployeeListItem } from '@/types/employee';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
employee: EmployeeListItem;
|
employee: EmployeeListItem;
|
||||||
@ -28,8 +28,10 @@ function resetPassword() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
resetPasswordConfirmOpen.value = false;
|
resetPasswordConfirmOpen.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal mereset kata sandi.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
resetPasswordProcessing.value = false;
|
resetPasswordProcessing.value = false;
|
||||||
@ -50,9 +52,7 @@ function resetPassword() {
|
|||||||
error-message="Gagal menghapus pegawai." />
|
error-message="Gagal menghapus pegawai." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog v-if="can('employees.reset_password')" v-model:open="resetPasswordConfirmOpen"
|
||||||
v-if="can('employees.reset_password')"
|
|
||||||
v-model:open="resetPasswordConfirmOpen"
|
|
||||||
title="Reset kata sandi pegawai?"
|
title="Reset kata sandi pegawai?"
|
||||||
:description="`Kata sandi ${employee.profile?.full_name ?? 'pegawai'} akan direset ke kata sandi default sistem (${defaultPassword}). Pengguna akan otomatis logout dari semua sesi aktif.`"
|
:description="`Kata sandi ${employee.profile?.full_name ?? 'pegawai'} akan direset ke kata sandi default sistem (${defaultPassword}). Pengguna akan otomatis logout dari semua sesi aktif.`"
|
||||||
confirm-label="Reset Kata Sandi" cancel-label="Batal" :loading="resetPasswordProcessing"
|
confirm-label="Reset Kata Sandi" cancel-label="Batal" :loading="resetPasswordProcessing"
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { toast } from 'vue-sonner';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { EmployeeListItem } from '@/types/employee';
|
|
||||||
import { toggle_status } from '@/routes/admin/hr/employees';
|
import { toggle_status } from '@/routes/admin/hr/employees';
|
||||||
|
import type { EmployeeListItem } from '@/types/employee';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
employee: EmployeeListItem;
|
employee: EmployeeListItem;
|
||||||
@ -37,9 +37,12 @@ function toggleStatus(checked: boolean) {
|
|||||||
is_active: checked,
|
is_active: checked,
|
||||||
}, {
|
}, {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
isActive.value = previous;
|
isActive.value = previous;
|
||||||
toast.error('Gagal memperbarui status pegawai.');
|
|
||||||
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
processing.value = false;
|
processing.value = false;
|
||||||
@ -50,11 +53,8 @@ function toggleStatus(checked: boolean) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Switch
|
<Switch :model-value="isActive" :disabled="processing || !can('employees.toggle_status')"
|
||||||
:model-value="isActive"
|
@update:model-value="toggleStatus" />
|
||||||
:disabled="processing || !can('employees.toggle_status')"
|
|
||||||
@update:model-value="toggleStatus"
|
|
||||||
/>
|
|
||||||
<Badge :variant="isActive ? 'default' : 'secondary'">
|
<Badge :variant="isActive ? 'default' : 'secondary'">
|
||||||
{{ isActive ? 'Aktif' : 'Nonaktif' }}
|
{{ isActive ? 'Aktif' : 'Nonaktif' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@ -21,8 +21,8 @@ import {
|
|||||||
} from '@/components/ui/field';
|
} from '@/components/ui/field';
|
||||||
import { useFormDialog } from '@/composables/useFormDialog';
|
import { useFormDialog } from '@/composables/useFormDialog';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import type { LeaveRequestFormData, LeaveRequestListItem } from '@/types/leave-request';
|
|
||||||
import { store, update } from '@/routes/admin/hr/leave_requests';
|
import { store, update } from '@/routes/admin/hr/leave_requests';
|
||||||
|
import type { LeaveRequestFormData, LeaveRequestListItem } from '@/types/leave-request';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -66,8 +66,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -21,8 +21,8 @@ import {
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
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 { LeaveRequestListItem } from '@/types/leave-request';
|
|
||||||
import { reject } from '@/routes/admin/hr/leave_requests';
|
import { reject } from '@/routes/admin/hr/leave_requests';
|
||||||
|
import type { LeaveRequestListItem } from '@/types/leave-request';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -55,8 +55,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menolak pengajuan cuti. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -74,8 +76,9 @@ function submit() {
|
|||||||
<FieldSet class="grid gap-4">
|
<FieldSet class="grid gap-4">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="reject-reason" required>Alasan Penolakan</FieldLabel>
|
<FieldLabel for="reject-reason" required>Alasan Penolakan</FieldLabel>
|
||||||
<Textarea id="reject-reason" v-model="form.reason" placeholder="Contoh: Kuota cuti untuk bulan ini sudah habis"
|
<Textarea id="reject-reason" v-model="form.reason"
|
||||||
rows="3" autofocus :maxlength="FIELD_LIMITS.reason" />
|
placeholder="Contoh: Kuota cuti untuk bulan ini sudah habis" rows="3" autofocus
|
||||||
|
:maxlength="FIELD_LIMITS.reason" />
|
||||||
<FieldError :errors="formErrors(form, 'reason')" />
|
<FieldError :errors="formErrors(form, 'reason')" />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { toast } from 'vue-sonner';
|
|||||||
import { RowApproveAction, RowDeleteAction, RowEditAction, RowRejectAction } from '@/components/button';
|
import { RowApproveAction, RowDeleteAction, RowEditAction, RowRejectAction } from '@/components/button';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { LeaveRequestListItem } from '@/types/leave-request';
|
|
||||||
import { destroy, approve } from '@/routes/admin/hr/leave_requests';
|
import { destroy, approve } from '@/routes/admin/hr/leave_requests';
|
||||||
|
import type { LeaveRequestListItem } from '@/types/leave-request';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
leaveRequest: LeaveRequestListItem;
|
leaveRequest: LeaveRequestListItem;
|
||||||
@ -50,8 +50,10 @@ function approveLeaveRequest() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
approveConfirmOpen.value = false;
|
approveConfirmOpen.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyetujui pengajuan cuti.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
approveProcessing.value = false;
|
approveProcessing.value = false;
|
||||||
|
|||||||
@ -36,6 +36,8 @@ import { getFirstCoverImage } from '@/lib/catalog-cover';
|
|||||||
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 { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
||||||
|
import draft_materials from '@/routes/admin/manage/cuttings/draft_materials';
|
||||||
|
import draft_results from '@/routes/admin/manage/cuttings/draft_results';
|
||||||
import type {
|
import type {
|
||||||
CuttingMaterialCartItem,
|
CuttingMaterialCartItem,
|
||||||
CuttingProductCatalogItem,
|
CuttingProductCatalogItem,
|
||||||
@ -44,8 +46,6 @@ import type {
|
|||||||
} from '@/types/cutting';
|
} from '@/types/cutting';
|
||||||
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
|
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
|
||||||
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
|
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
|
||||||
import draft_materials from '@/routes/admin/manage/cuttings/draft_materials';
|
|
||||||
import draft_results from '@/routes/admin/manage/cuttings/draft_results';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
|
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
|
||||||
@ -127,17 +127,6 @@ function materialLineCost(item: CuttingMaterialCartItem): number {
|
|||||||
return Math.round(usage * catalogPrice.price);
|
return Math.round(usage * catalogPrice.price);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sewingCostAmount = computed(
|
|
||||||
() => Number.parseInt(parseRupiah(form.sewing_cost), 10) || 0,
|
|
||||||
);
|
|
||||||
const otherCostAmount = computed(
|
|
||||||
() => Number.parseInt(parseRupiah(form.other_cost), 10) || 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalProductionCost = computed(
|
|
||||||
() => totalMaterialCost.value + sewingCostAmount.value + otherCostAmount.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
const estimatedCostPerUnit = computed(() => {
|
const estimatedCostPerUnit = computed(() => {
|
||||||
if (totalResultPieces.value <= 0) {
|
if (totalResultPieces.value <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -212,7 +201,7 @@ const filteredProducts = computed(() => {
|
|||||||
return props.productCatalog.filter(
|
return props.productCatalog.filter(
|
||||||
(product) =>
|
(product) =>
|
||||||
product.name.toLowerCase().includes(keyword) ||
|
product.name.toLowerCase().includes(keyword) ||
|
||||||
product.variants.some((variant) =>
|
product.variants.some((variant: any) =>
|
||||||
variant.name.toLowerCase().includes(keyword),
|
variant.name.toLowerCase().includes(keyword),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -594,13 +583,10 @@ function submit() {
|
|||||||
|
|
||||||
if (props.method === 'put') {
|
if (props.method === 'put') {
|
||||||
form.transform(() => payload).put(props.submitUrl, {
|
form.transform(() => payload).put(props.submitUrl, {
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
toast.error(
|
toast.error(errors.system);
|
||||||
typeof message === 'string'
|
}
|
||||||
? message
|
|
||||||
: 'Gagal menyimpan data. Periksa kembali formulir.',
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -608,13 +594,10 @@ function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.transform(() => payload).post(props.submitUrl, {
|
form.transform(() => payload).post(props.submitUrl, {
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
toast.error(
|
toast.error(errors.system);
|
||||||
typeof message === 'string'
|
}
|
||||||
? message
|
|
||||||
: 'Gagal menyimpan data. Periksa kembali formulir.',
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -630,26 +613,15 @@ function submit() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Search
|
<Search class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"
|
<Input v-model="materialSearch" placeholder="Cari bahan baku..." class="pl-9" />
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
v-model="materialSearch"
|
|
||||||
placeholder="Cari bahan baku..."
|
|
||||||
class="pl-9"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="filteredRawMaterials.length === 0" class="py-8">
|
||||||
v-if="filteredRawMaterials.length === 0"
|
|
||||||
class="py-8"
|
|
||||||
>
|
|
||||||
<Empty>
|
<Empty>
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyTitle
|
<EmptyTitle>Tidak ada bahan baku
|
||||||
>Tidak ada bahan baku
|
ditemukan</EmptyTitle>
|
||||||
ditemukan</EmptyTitle
|
|
||||||
>
|
|
||||||
<EmptyDescription>
|
<EmptyDescription>
|
||||||
Silakan lakukan pencarian untuk
|
Silakan lakukan pencarian untuk
|
||||||
menemukan bahan baku.
|
menemukan bahan baku.
|
||||||
@ -659,97 +631,60 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="columns-1 gap-4 sm:columns-2">
|
<div v-else class="columns-1 gap-4 sm:columns-2">
|
||||||
<PosCatalogCard
|
<PosCatalogCard v-for="rawMaterial in filteredRawMaterials" :key="rawMaterial.id"
|
||||||
v-for="rawMaterial in filteredRawMaterials"
|
:title="rawMaterial.name" :cover-image="getFirstCoverImage(rawMaterial.prices)
|
||||||
:key="rawMaterial.id"
|
">
|
||||||
:title="rawMaterial.name"
|
|
||||||
:cover-image="
|
|
||||||
getFirstCoverImage(rawMaterial.prices)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<Badge variant="secondary" class="mt-1.5">
|
<Badge variant="secondary" class="mt-1.5">
|
||||||
{{ rawMaterial.unit_label }}
|
{{ rawMaterial.unit_label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<p
|
<p v-if="!rawMaterial.prices.length" class="px-3 py-4 text-sm text-muted-foreground">
|
||||||
v-if="!rawMaterial.prices.length"
|
|
||||||
class="px-3 py-4 text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada varian
|
Belum ada varian
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div v-for="price in rawMaterial.prices" :key="price.id"
|
||||||
v-for="price in rawMaterial.prices"
|
class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200" :class="[
|
||||||
:key="price.id"
|
|
||||||
class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200"
|
|
||||||
:class="[
|
|
||||||
'cursor-pointer hover:bg-muted/30',
|
'cursor-pointer hover:bg-muted/30',
|
||||||
getMaterialCartItem(price.id)
|
getMaterialCartItem(price.id)
|
||||||
? 'mx-1 my-0.5 rounded-md border-2 border-primary bg-primary/5'
|
? 'mx-1 my-0.5 rounded-md border-2 border-primary bg-primary/5'
|
||||||
: '',
|
: '',
|
||||||
]"
|
]" @click="
|
||||||
@click="
|
|
||||||
!getMaterialCartItem(price.id) &&
|
!getMaterialCartItem(price.id) &&
|
||||||
addMaterial(rawMaterial, price)
|
addMaterial(rawMaterial, price)
|
||||||
"
|
">
|
||||||
>
|
<PosCatalogVariantThumb :items="price.images" />
|
||||||
<PosCatalogVariantThumb
|
|
||||||
:items="price.images"
|
|
||||||
/>
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-sm font-medium">
|
<p class="truncate text-sm font-medium">
|
||||||
{{ price.variant }}
|
{{ price.variant }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p class="text-xs text-muted-foreground tabular-nums">
|
||||||
class="text-xs text-muted-foreground tabular-nums"
|
|
||||||
>
|
|
||||||
Stok: {{ price.stock_formatted }}
|
Stok: {{ price.stock_formatted }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="getMaterialCartItem(price.id)"
|
||||||
v-if="getMaterialCartItem(price.id)"
|
class="flex shrink-0 items-center gap-1.5">
|
||||||
class="flex shrink-0 items-center gap-1.5"
|
<Button type="button" variant="outline" size="icon-sm" @click.stop="
|
||||||
>
|
decreaseMaterialQty(price.id)
|
||||||
<Button
|
">
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon-sm"
|
|
||||||
@click.stop="
|
|
||||||
decreaseMaterialQty(price.id)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Minus class="size-3.5" />
|
<Minus class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<span
|
<span class="min-w-[1.25rem] text-center text-xs font-semibold tabular-nums">
|
||||||
class="min-w-[1.25rem] text-center text-xs font-semibold tabular-nums"
|
|
||||||
>
|
|
||||||
{{
|
{{
|
||||||
getMaterialCartItem(price.id)!
|
getMaterialCartItem(price.id)!
|
||||||
.material_usage
|
.material_usage
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button type="button" variant="outline" size="icon-sm" @click.stop="
|
||||||
type="button"
|
addMaterial(rawMaterial, price)
|
||||||
variant="outline"
|
">
|
||||||
size="icon-sm"
|
|
||||||
@click.stop="
|
|
||||||
addMaterial(rawMaterial, price)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Plus class="size-3.5" />
|
<Plus class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button v-else type="button" variant="outline" size="icon-sm" class="shrink-0"
|
||||||
v-else
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon-sm"
|
|
||||||
class="shrink-0"
|
|
||||||
@click.stop="
|
@click.stop="
|
||||||
addMaterial(rawMaterial, price)
|
addMaterial(rawMaterial, price)
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<Plus class="size-3.5" />
|
<Plus class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -766,22 +701,14 @@ function submit() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Search
|
<Search class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"
|
<Input v-model="productSearch" placeholder="Cari produk..." class="pl-9" />
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
v-model="productSearch"
|
|
||||||
placeholder="Cari produk..."
|
|
||||||
class="pl-9"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="filteredProducts.length === 0" class="py-8">
|
<div v-if="filteredProducts.length === 0" class="py-8">
|
||||||
<Empty>
|
<Empty>
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyTitle
|
<EmptyTitle>Tidak ada produk ditemukan</EmptyTitle>
|
||||||
>Tidak ada produk ditemukan</EmptyTitle
|
|
||||||
>
|
|
||||||
<EmptyDescription>
|
<EmptyDescription>
|
||||||
Silakan lakukan pencarian untuk
|
Silakan lakukan pencarian untuk
|
||||||
menemukan produk.
|
menemukan produk.
|
||||||
@ -791,94 +718,55 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="columns-1 gap-4 sm:columns-2">
|
<div v-else class="columns-1 gap-4 sm:columns-2">
|
||||||
<PosCatalogCard
|
<PosCatalogCard v-for="product in filteredProducts" :key="product.id" :title="product.name"
|
||||||
v-for="product in filteredProducts"
|
:cover-image="getFirstCoverImage(product.variants)
|
||||||
:key="product.id"
|
">
|
||||||
:title="product.name"
|
<p v-if="!product.variants.length" class="px-3 py-4 text-sm text-muted-foreground">
|
||||||
:cover-image="
|
|
||||||
getFirstCoverImage(product.variants)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
v-if="!product.variants.length"
|
|
||||||
class="px-3 py-4 text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada varian
|
Belum ada varian
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div v-for="variant in product.variants" :key="variant.id"
|
||||||
v-for="variant in product.variants"
|
class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200" :class="[
|
||||||
:key="variant.id"
|
|
||||||
class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200"
|
|
||||||
:class="[
|
|
||||||
'cursor-pointer hover:bg-muted/30',
|
'cursor-pointer hover:bg-muted/30',
|
||||||
getResultCartItem(variant.id)
|
getResultCartItem(variant.id)
|
||||||
? 'mx-1 my-0.5 rounded-md border-2 border-primary bg-primary/5'
|
? 'mx-1 my-0.5 rounded-md border-2 border-primary bg-primary/5'
|
||||||
: '',
|
: '',
|
||||||
]"
|
]" @click="
|
||||||
@click="
|
|
||||||
!getResultCartItem(variant.id) &&
|
!getResultCartItem(variant.id) &&
|
||||||
addResult(product, variant)
|
addResult(product, variant)
|
||||||
"
|
">
|
||||||
>
|
<PosCatalogVariantThumb :items="variant.images" />
|
||||||
<PosCatalogVariantThumb
|
|
||||||
:items="variant.images"
|
|
||||||
/>
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-sm font-medium">
|
<p class="truncate text-sm font-medium">
|
||||||
{{ variant.name }}
|
{{ variant.name }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p class="text-xs text-muted-foreground">
|
||||||
class="text-xs text-muted-foreground"
|
<span class="tabular-nums">Stok:
|
||||||
>
|
{{ variant.stock }} pcs</span>
|
||||||
<span class="tabular-nums"
|
|
||||||
>Stok:
|
|
||||||
{{ variant.stock }} pcs</span
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="getResultCartItem(variant.id)"
|
||||||
v-if="getResultCartItem(variant.id)"
|
class="flex shrink-0 items-center gap-1.5">
|
||||||
class="flex shrink-0 items-center gap-1.5"
|
<Button type="button" variant="outline" size="icon-sm" @click.stop="
|
||||||
>
|
decreaseResultQty(variant.id)
|
||||||
<Button
|
">
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon-sm"
|
|
||||||
@click.stop="
|
|
||||||
decreaseResultQty(variant.id)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Minus class="size-3.5" />
|
<Minus class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<span
|
<span class="min-w-[1.25rem] text-center text-xs font-semibold tabular-nums">
|
||||||
class="min-w-[1.25rem] text-center text-xs font-semibold tabular-nums"
|
|
||||||
>
|
|
||||||
{{
|
{{
|
||||||
getResultCartItem(variant.id)!
|
getResultCartItem(variant.id)!
|
||||||
.cutting_result
|
.cutting_result
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button type="button" variant="outline" size="icon-sm" @click.stop="
|
||||||
type="button"
|
addResult(product, variant)
|
||||||
variant="outline"
|
">
|
||||||
size="icon-sm"
|
|
||||||
@click.stop="
|
|
||||||
addResult(product, variant)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Plus class="size-3.5" />
|
<Plus class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button v-else type="button" variant="outline" size="icon-sm" class="shrink-0"
|
||||||
v-else
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon-sm"
|
|
||||||
class="shrink-0"
|
|
||||||
@click.stop="
|
@click.stop="
|
||||||
addResult(product, variant)
|
addResult(product, variant)
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<Plus class="size-3.5" />
|
<Plus class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -897,19 +785,12 @@ function submit() {
|
|||||||
Ringkasan Cutting
|
Ringkasan Cutting
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<button
|
<button v-if="materialCart.length > 0 || resultCart.length > 0" type="button"
|
||||||
v-if="materialCart.length > 0 || resultCart.length > 0"
|
|
||||||
type="button"
|
|
||||||
class="text-xs font-normal text-primary underline underline-offset-2 hover:text-primary/80"
|
class="text-xs font-normal text-primary underline underline-offset-2 hover:text-primary/80"
|
||||||
@click="cartDetailOpen = true"
|
@click="cartDetailOpen = true">
|
||||||
>
|
|
||||||
Lihat Detail
|
Lihat Detail
|
||||||
</button>
|
</button>
|
||||||
<Badge
|
<Badge v-if="totalResultPieces > 0" variant="secondary" class="tabular-nums font-semibold">
|
||||||
v-if="totalResultPieces > 0"
|
|
||||||
variant="secondary"
|
|
||||||
class="tabular-nums font-semibold"
|
|
||||||
>
|
|
||||||
Total: {{ totalResultPieces }} pcs
|
Total: {{ totalResultPieces }} pcs
|
||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
@ -920,61 +801,39 @@ function submit() {
|
|||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<FieldSet class="grid gap-4">
|
<FieldSet class="grid gap-4">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="description"
|
<FieldLabel for="description">Keterangan</FieldLabel>
|
||||||
>Keterangan</FieldLabel
|
<Textarea id="description" v-model="form.description"
|
||||||
>
|
placeholder="Contoh: Cutting batch pagi" rows="2"
|
||||||
<Textarea
|
:maxlength="FIELD_LIMITS.description" />
|
||||||
id="description"
|
<FieldError :errors="formErrors(form, 'description')" />
|
||||||
v-model="form.description"
|
|
||||||
placeholder="Contoh: Cutting batch pagi"
|
|
||||||
rows="2"
|
|
||||||
:maxlength="FIELD_LIMITS.description"
|
|
||||||
/>
|
|
||||||
<FieldError
|
|
||||||
:errors="formErrors(form, 'description')"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-sm font-medium">Bahan Baku</p>
|
<p class="text-sm font-medium">Bahan Baku</p>
|
||||||
|
|
||||||
<div
|
<div v-if="materialCart.length === 0"
|
||||||
v-if="materialCart.length === 0"
|
class="rounded-lg border border-dashed px-4 py-6 text-center text-sm text-muted-foreground">
|
||||||
class="rounded-lg border border-dashed px-4 py-6 text-center text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada bahan baku dipilih.
|
Belum ada bahan baku dipilih.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
|
<div v-else
|
||||||
<div
|
class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
|
||||||
v-for="(item, index) in materialCart"
|
<div v-for="(item, index) in materialCart" :key="item.raw_material_price_id"
|
||||||
:key="item.raw_material_price_id"
|
class="rounded-lg border p-3">
|
||||||
class="rounded-lg border p-3"
|
<div class="mb-2 flex items-start justify-between gap-2">
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mb-2 flex items-start justify-between gap-2"
|
|
||||||
>
|
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p
|
<p class="truncate text-sm font-medium">
|
||||||
class="truncate text-sm font-medium"
|
|
||||||
>
|
|
||||||
{{ item.raw_material_name }}
|
{{ item.raw_material_name }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p class="truncate text-xs text-muted-foreground">
|
||||||
class="truncate text-xs text-muted-foreground"
|
|
||||||
>
|
|
||||||
{{ item.variant }} · Stok
|
{{ item.variant }} · Stok
|
||||||
{{ item.stock_input }}
|
{{ item.stock_input }}
|
||||||
{{ item.unit_abbreviation }}
|
{{ item.unit_abbreviation }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="button" variant="ghost" size="icon"
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="size-7 shrink-0 text-destructive hover:text-destructive"
|
class="size-7 shrink-0 text-destructive hover:text-destructive"
|
||||||
@click="removeMaterial(index)"
|
@click="removeMaterial(index)">
|
||||||
>
|
|
||||||
<Trash2 class="size-3.5" />
|
<Trash2 class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -984,33 +843,23 @@ function submit() {
|
|||||||
<FieldLabel class="text-xs">
|
<FieldLabel class="text-xs">
|
||||||
Pemakaian ({{ item.unit_abbreviation }})
|
Pemakaian ({{ item.unit_abbreviation }})
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<DecimalInput
|
<DecimalInput v-model="item.material_usage
|
||||||
v-model="
|
" class="h-8" @change="
|
||||||
item.material_usage
|
|
||||||
"
|
|
||||||
class="h-8"
|
|
||||||
@change="
|
|
||||||
syncMaterialField(
|
syncMaterialField(
|
||||||
index,
|
index,
|
||||||
)
|
)
|
||||||
"
|
" />
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel class="text-xs">
|
<FieldLabel class="text-xs">
|
||||||
Sisa ({{ item.unit_abbreviation }})
|
Sisa ({{ item.unit_abbreviation }})
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<DecimalInput
|
<DecimalInput v-model="item.remaining_material
|
||||||
v-model="
|
" class="h-8" @change="
|
||||||
item.remaining_material
|
|
||||||
"
|
|
||||||
class="h-8"
|
|
||||||
@change="
|
|
||||||
syncMaterialField(
|
syncMaterialField(
|
||||||
index,
|
index,
|
||||||
)
|
)
|
||||||
"
|
" />
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1022,111 +871,68 @@ function submit() {
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<p class="text-sm font-medium">Hasil Produk</p>
|
<p class="text-sm font-medium">Hasil Produk</p>
|
||||||
<Badge
|
<Badge v-if="totalResultPieces > 0" variant="outline" class="tabular-nums text-xs">
|
||||||
v-if="totalResultPieces > 0"
|
|
||||||
variant="outline"
|
|
||||||
class="tabular-nums text-xs"
|
|
||||||
>
|
|
||||||
{{ totalResultPieces }} pcs
|
{{ totalResultPieces }} pcs
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="resultCart.length === 0"
|
||||||
v-if="resultCart.length === 0"
|
class="rounded-lg border border-dashed px-4 py-6 text-center text-sm text-muted-foreground">
|
||||||
class="rounded-lg border border-dashed px-4 py-6 text-center text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada produk hasil dipilih.
|
Belum ada produk hasil dipilih.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
|
<div v-else
|
||||||
<div
|
class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
|
||||||
v-for="(item, index) in resultCart"
|
<div v-for="(item, index) in resultCart" :key="item.product_variant_id"
|
||||||
:key="item.product_variant_id"
|
class="rounded-lg border p-3">
|
||||||
class="rounded-lg border p-3"
|
<div class="mb-2 flex items-start justify-between gap-2">
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mb-2 flex items-start justify-between gap-2"
|
|
||||||
>
|
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p
|
<p class="truncate text-sm font-medium">
|
||||||
class="truncate text-sm font-medium"
|
|
||||||
>
|
|
||||||
{{ item.product_name }}
|
{{ item.product_name }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p class="truncate text-xs text-muted-foreground">
|
||||||
class="truncate text-xs text-muted-foreground"
|
|
||||||
>
|
|
||||||
{{ item.variant_name }} ·
|
{{ item.variant_name }} ·
|
||||||
Stok {{ item.stock }} pcs
|
Stok {{ item.stock }} pcs
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="button" variant="ghost" size="icon"
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="size-7 shrink-0 text-destructive hover:text-destructive"
|
class="size-7 shrink-0 text-destructive hover:text-destructive"
|
||||||
@click="removeResult(index)"
|
@click="removeResult(index)">
|
||||||
>
|
|
||||||
<Trash2 class="size-3.5" />
|
<Trash2 class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel class="text-xs"
|
<FieldLabel class="text-xs">Hasil</FieldLabel>
|
||||||
>Hasil</FieldLabel
|
<NumberInput v-model="item.cutting_result
|
||||||
>
|
" class="h-8" @change="
|
||||||
<NumberInput
|
|
||||||
v-model="
|
|
||||||
item.cutting_result
|
|
||||||
"
|
|
||||||
class="h-8"
|
|
||||||
@change="
|
|
||||||
syncResultTotals(item);
|
syncResultTotals(item);
|
||||||
syncResultField(index)
|
syncResultField(index)
|
||||||
"
|
" />
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel class="text-xs"
|
<FieldLabel class="text-xs">Bagus</FieldLabel>
|
||||||
>Bagus</FieldLabel
|
<NumberInput v-model="item.warehouse_stock
|
||||||
>
|
" class="h-8" @change="
|
||||||
<NumberInput
|
|
||||||
v-model="
|
|
||||||
item.warehouse_stock
|
|
||||||
"
|
|
||||||
class="h-8"
|
|
||||||
@change="
|
|
||||||
syncResultTotals(item);
|
syncResultTotals(item);
|
||||||
syncResultField(index)
|
syncResultField(index)
|
||||||
"
|
" />
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel class="text-xs"
|
<FieldLabel class="text-xs">Reject</FieldLabel>
|
||||||
>Reject</FieldLabel
|
<NumberInput v-model="item.cutting_reject
|
||||||
>
|
" class="h-8" />
|
||||||
<NumberInput
|
|
||||||
v-model="
|
|
||||||
item.cutting_reject
|
|
||||||
"
|
|
||||||
class="h-8"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" class="w-full" :disabled="form.processing ||
|
||||||
type="submit"
|
materialCart.length === 0 ||
|
||||||
class="w-full"
|
resultCart.length === 0
|
||||||
:disabled="
|
">
|
||||||
form.processing ||
|
|
||||||
materialCart.length === 0 ||
|
|
||||||
resultCart.length === 0
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Save class="size-4" />
|
<Save class="size-4" />
|
||||||
{{
|
{{
|
||||||
form.processing
|
form.processing
|
||||||
@ -1158,11 +964,8 @@ function submit() {
|
|||||||
<span class="text-muted-foreground">Bahan Baku</span>
|
<span class="text-muted-foreground">Bahan Baku</span>
|
||||||
<span class="font-medium tabular-nums">{{ formatRupiah(totalMaterialCost) }}</span>
|
<span class="font-medium tabular-nums">{{ formatRupiah(totalMaterialCost) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-for="item in materialCart" :key="`cost-${item.raw_material_price_id}`"
|
||||||
v-for="item in materialCart"
|
class="flex items-center justify-between gap-3 text-xs">
|
||||||
:key="`cost-${item.raw_material_price_id}`"
|
|
||||||
class="flex items-center justify-between gap-3 text-xs"
|
|
||||||
>
|
|
||||||
<span class="truncate text-muted-foreground">
|
<span class="truncate text-muted-foreground">
|
||||||
{{ item.raw_material_name }} ({{ item.variant }})
|
{{ item.raw_material_name }} ({{ item.variant }})
|
||||||
</span>
|
</span>
|
||||||
@ -1176,10 +979,8 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="totalResultPieces > 0"
|
||||||
v-if="totalResultPieces > 0"
|
class="flex items-center justify-between rounded-lg border border-primary/30 bg-primary/5 px-3 py-2 font-semibold">
|
||||||
class="flex items-center justify-between rounded-lg border border-primary/30 bg-primary/5 px-3 py-2 font-semibold"
|
|
||||||
>
|
|
||||||
<span>Harga Modal / pcs</span>
|
<span>Harga Modal / pcs</span>
|
||||||
<span class="tabular-nums">{{ formatRupiah(estimatedCostPerUnit) }}</span>
|
<span class="tabular-nums">{{ formatRupiah(estimatedCostPerUnit) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -1189,18 +990,10 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter class="gap-2 sm:justify-end">
|
<DialogFooter class="gap-2 sm:justify-end">
|
||||||
<Button
|
<Button type="button" variant="outline" @click="costModalOpen = false">
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
@click="costModalOpen = false"
|
|
||||||
>
|
|
||||||
Tutup
|
Tutup
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="button" :disabled="form.processing" @click="confirmSubmit">
|
||||||
type="button"
|
|
||||||
:disabled="form.processing"
|
|
||||||
@click="confirmSubmit"
|
|
||||||
>
|
|
||||||
{{ form.processing ? 'Menyimpan...' : submitLabel }}
|
{{ form.processing ? 'Menyimpan...' : submitLabel }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@ -1216,11 +1009,8 @@ function submit() {
|
|||||||
<div v-if="materialCart.length > 0">
|
<div v-if="materialCart.length > 0">
|
||||||
<p class="mb-2 text-xs font-medium text-muted-foreground">Bahan Baku</p>
|
<p class="mb-2 text-xs font-medium text-muted-foreground">Bahan Baku</p>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div v-for="item in materialCart" :key="`detail-mat-${item.raw_material_price_id}`"
|
||||||
v-for="item in materialCart"
|
class="rounded-lg border p-3">
|
||||||
:key="`detail-mat-${item.raw_material_price_id}`"
|
|
||||||
class="rounded-lg border p-3"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate text-sm font-medium">{{ item.raw_material_name }}</p>
|
<p class="truncate text-sm font-medium">{{ item.raw_material_name }}</p>
|
||||||
@ -1234,7 +1024,8 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
<div class="mt-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||||
<span>Sisa: {{ item.remaining_material }} {{ item.unit_abbreviation }}</span>
|
<span>Sisa: {{ item.remaining_material }} {{ item.unit_abbreviation }}</span>
|
||||||
<span class="font-medium text-foreground">{{ formatRupiah(materialLineCost(item)) }}</span>
|
<span class="font-medium text-foreground">{{ formatRupiah(materialLineCost(item))
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1242,11 +1033,8 @@ function submit() {
|
|||||||
<div v-if="resultCart.length > 0">
|
<div v-if="resultCart.length > 0">
|
||||||
<p class="mb-2 text-xs font-medium text-muted-foreground">Hasil Produk</p>
|
<p class="mb-2 text-xs font-medium text-muted-foreground">Hasil Produk</p>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div v-for="item in resultCart" :key="`detail-res-${item.product_variant_id}`"
|
||||||
v-for="item in resultCart"
|
class="rounded-lg border p-3">
|
||||||
:key="`detail-res-${item.product_variant_id}`"
|
|
||||||
class="rounded-lg border p-3"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate text-sm font-medium">{{ item.product_name }}</p>
|
<p class="truncate text-sm font-medium">{{ item.product_name }}</p>
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link, router, useForm } from '@inertiajs/vue3';
|
import { router, useForm } from '@inertiajs/vue3';
|
||||||
import { Check, RotateCcw, Scissors, X } from '@lucide/vue';
|
import { Check, RotateCcw, Scissors, X } from '@lucide/vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { RowDeleteAction, RowEditAction, RowStatusAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction, RowStatusAction } from '@/components/button';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@ -27,15 +28,14 @@ import { Switch } from '@/components/ui/switch';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import { CuttingStatus } from '@/constants/cutting-status';
|
import { CuttingStatus } from '@/constants/cutting-status';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
|
import { edit, destroy, transition_status } from '@/routes/admin/manage/cuttings';
|
||||||
|
import type { CuttingListItem, CuttingStatusAction } from '@/types/cutting';
|
||||||
import {
|
import {
|
||||||
PRICE_TYPES,
|
PRICE_TYPES,
|
||||||
PRICE_TYPE_LABELS,
|
PRICE_TYPE_LABELS,
|
||||||
} from '@/types/product';
|
} from '@/types/product';
|
||||||
import type { CuttingListItem, CuttingStatusAction } from '@/types/cutting';
|
|
||||||
import { edit, destroy, transition_status } from '@/routes/admin/manage/cuttings';
|
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cutting: CuttingListItem;
|
cutting: CuttingListItem;
|
||||||
@ -122,6 +122,7 @@ function setAllMatches(value: boolean) {
|
|||||||
}
|
}
|
||||||
function setResultPrice(resultIndex: number, type: string, value: string) {
|
function setResultPrice(resultIndex: number, type: string, value: string) {
|
||||||
const result = verifyForm.results[resultIndex];
|
const result = verifyForm.results[resultIndex];
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -203,20 +204,16 @@ function submitVerify() {
|
|||||||
result_prices: buildResultPricesPayload(),
|
result_prices: buildResultPricesPayload(),
|
||||||
}))
|
}))
|
||||||
.post(transition_status.url(props.cutting.id), {
|
.post(transition_status.url(props.cutting.id), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
verifyDialogOpen.value = false;
|
verifyDialogOpen.value = false;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
toast.error(
|
}
|
||||||
typeof message === 'string'
|
},
|
||||||
? message
|
});
|
||||||
: 'Gagal memverifikasi cutting.',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function transitionStatus() {
|
function transitionStatus() {
|
||||||
@ -237,14 +234,10 @@ function transitionStatus() {
|
|||||||
statusConfirmOpen.value = false;
|
statusConfirmOpen.value = false;
|
||||||
pendingAction.value = null;
|
pendingAction.value = null;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
toast.error(
|
}
|
||||||
typeof message === 'string'
|
|
||||||
? message
|
|
||||||
: 'Gagal memperbarui status cutting.',
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
statusProcessing.value = false;
|
statusProcessing.value = false;
|
||||||
@ -259,14 +252,10 @@ function submitReject() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
rejectDialogOpen.value = false;
|
rejectDialogOpen.value = false;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
toast.error(
|
}
|
||||||
typeof message === 'string'
|
|
||||||
? message
|
|
||||||
: 'Gagal menolak cutting.',
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -298,9 +287,8 @@ function actionIcon(status: string) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-center justify-end gap-1">
|
<div class="flex flex-wrap items-center justify-end gap-1">
|
||||||
<template v-for="action in availableActions" :key="action.status">
|
<template v-for="action in availableActions" :key="action.status">
|
||||||
<RowStatusAction v-if="canPerformAction(action)" :size="'sm'"
|
<RowStatusAction v-if="canPerformAction(action)" :size="'sm'" :icon="actionIcon(action.status)"
|
||||||
:icon="actionIcon(action.status)" :label="action.label" :destructive="action.destructive"
|
:label="action.label" :destructive="action.destructive" @click="openStatusConfirm(action)" />
|
||||||
@click="openStatusConfirm(action)" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<RowEditAction v-if="canEdit" :href="edit.url(cutting.id)" />
|
<RowEditAction v-if="canEdit" :href="edit.url(cutting.id)" />
|
||||||
@ -311,22 +299,12 @@ function actionIcon(status: string) {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog v-model:open="statusConfirmOpen" :title="pendingAction
|
||||||
v-model:open="statusConfirmOpen"
|
? `${pendingAction.label} cutting?`
|
||||||
:title="
|
: 'Ubah status cutting?'
|
||||||
pendingAction
|
" :description="pendingAction ? statusConfirmDescription(pendingAction) : ''
|
||||||
? `${pendingAction.label} cutting?`
|
" :confirm-label="pendingAction?.label ?? 'Konfirmasi'" cancel-label="Batal"
|
||||||
: 'Ubah status cutting?'
|
:destructive="pendingAction?.destructive ?? false" :loading="statusProcessing" @confirm="transitionStatus" />
|
||||||
"
|
|
||||||
:description="
|
|
||||||
pendingAction ? statusConfirmDescription(pendingAction) : ''
|
|
||||||
"
|
|
||||||
:confirm-label="pendingAction?.label ?? 'Konfirmasi'"
|
|
||||||
cancel-label="Batal"
|
|
||||||
:destructive="pendingAction?.destructive ?? false"
|
|
||||||
:loading="statusProcessing"
|
|
||||||
@confirm="transitionStatus"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dialog v-model:open="rejectDialogOpen">
|
<Dialog v-model:open="rejectDialogOpen">
|
||||||
<DialogContent class="sm:max-w-md">
|
<DialogContent class="sm:max-w-md">
|
||||||
@ -338,42 +316,24 @@ function actionIcon(status: string) {
|
|||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<FieldSet class="grid gap-4">
|
<FieldSet class="grid gap-4">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="cutting-reject-reason" required
|
<FieldLabel for="cutting-reject-reason" required>Alasan Penolakan</FieldLabel>
|
||||||
>Alasan Penolakan</FieldLabel
|
<Textarea id="cutting-reject-reason" v-model="rejectForm.reason"
|
||||||
>
|
placeholder="Contoh: Jumlah barang yang diterima tidak sesuai" rows="3" autofocus
|
||||||
<Textarea
|
:maxlength="FIELD_LIMITS.reason" />
|
||||||
id="cutting-reject-reason"
|
<FieldError :errors="rejectForm.errors.reason
|
||||||
v-model="rejectForm.reason"
|
? [rejectForm.errors.reason]
|
||||||
placeholder="Contoh: Jumlah barang yang diterima tidak sesuai"
|
: []
|
||||||
rows="3"
|
" />
|
||||||
autofocus
|
|
||||||
:maxlength="FIELD_LIMITS.reason"
|
|
||||||
/>
|
|
||||||
<FieldError
|
|
||||||
:errors="
|
|
||||||
rejectForm.errors.reason
|
|
||||||
? [rejectForm.errors.reason]
|
|
||||||
: []
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|
||||||
<DialogFooter class="mt-6">
|
<DialogFooter class="mt-6">
|
||||||
<Button
|
<Button type="button" variant="outline" :disabled="rejectForm.processing"
|
||||||
type="button"
|
@click="rejectDialogOpen = false">
|
||||||
variant="outline"
|
|
||||||
:disabled="rejectForm.processing"
|
|
||||||
@click="rejectDialogOpen = false"
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" variant="destructive" :disabled="rejectForm.processing">
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
:disabled="rejectForm.processing"
|
|
||||||
>
|
|
||||||
{{ rejectForm.processing ? 'Menyimpan...' : 'Tolak' }}
|
{{ rejectForm.processing ? 'Menyimpan...' : 'Tolak' }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@ -397,15 +357,12 @@ function actionIcon(status: string) {
|
|||||||
<Label for="all-matches" class="text-sm font-medium">
|
<Label for="all-matches" class="text-sm font-medium">
|
||||||
Semua sesuai dengan data cutting
|
Semua sesuai dengan data cutting
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch id="all-matches" :model-value="allMatches" @update:model-value="setAllMatches" />
|
||||||
id="all-matches"
|
|
||||||
:model-value="allMatches"
|
|
||||||
@update:model-value="setAllMatches"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id" class="p-3 border rounded-lg space-y-3">
|
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id"
|
||||||
|
class="p-3 border rounded-lg space-y-3">
|
||||||
<div class="font-medium text-sm">
|
<div class="font-medium text-sm">
|
||||||
{{ result.name }}
|
{{ result.name }}
|
||||||
</div>
|
</div>
|
||||||
@ -430,30 +387,21 @@ function actionIcon(status: string) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-2 items-end text-xs">
|
<div class="grid grid-cols-2 gap-2 items-end text-xs">
|
||||||
<div>
|
<div>
|
||||||
<label :for="`good-${index}`" class="text-muted-foreground block mb-0.5">Stok Bagus (diterima):</label>
|
<label :for="`good-${index}`" class="text-muted-foreground block mb-0.5">Stok Bagus
|
||||||
<Input
|
(diterima):</label>
|
||||||
:id="`good-${index}`"
|
<Input :id="`good-${index}`" type="number" v-model.number="result.warehouse_stock"
|
||||||
type="number"
|
min="0" :max="result.cutting_result" class="h-8 w-full px-2 text-xs"
|
||||||
v-model.number="result.warehouse_stock"
|
|
||||||
min="0"
|
|
||||||
:max="result.cutting_result"
|
|
||||||
class="h-8 w-full px-2 text-xs"
|
|
||||||
:disabled="allMatches"
|
:disabled="allMatches"
|
||||||
@input="result.cutting_reject = result.cutting_result - result.warehouse_stock"
|
@input="result.cutting_reject = result.cutting_result - result.warehouse_stock" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label :for="`reject-${index}`" class="text-muted-foreground block mb-0.5">Stok Reject (diterima):</label>
|
<label :for="`reject-${index}`" class="text-muted-foreground block mb-0.5">Stok
|
||||||
<Input
|
Reject
|
||||||
:id="`reject-${index}`"
|
(diterima):</label>
|
||||||
type="number"
|
<Input :id="`reject-${index}`" type="number" v-model.number="result.cutting_reject"
|
||||||
v-model.number="result.cutting_reject"
|
min="0" :max="result.cutting_result" class="h-8 w-full px-2 text-xs"
|
||||||
min="0"
|
|
||||||
:max="result.cutting_result"
|
|
||||||
class="h-8 w-full px-2 text-xs"
|
|
||||||
:disabled="allMatches"
|
:disabled="allMatches"
|
||||||
@input="result.warehouse_stock = result.cutting_result - result.cutting_reject"
|
@input="result.warehouse_stock = result.cutting_result - result.cutting_reject" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -462,47 +410,30 @@ function actionIcon(status: string) {
|
|||||||
<FieldLabel class="text-xs" :for="`price-${index}-${type}`">
|
<FieldLabel class="text-xs" :for="`price-${index}-${type}`">
|
||||||
{{ PRICE_TYPE_LABELS[type] }}
|
{{ PRICE_TYPE_LABELS[type] }}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<RupiahInput
|
<RupiahInput :id="`price-${index}-${type}`" :model-value="result.prices[type]"
|
||||||
:id="`price-${index}-${type}`"
|
@update:model-value="setResultPrice(index, type, $event)" />
|
||||||
:model-value="result.prices[type]"
|
|
||||||
@update:model-value="setResultPrice(index, type, $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FieldError
|
<FieldError :errors="verifyForm.errors.result_prices ? [verifyForm.errors.result_prices] : []" />
|
||||||
:errors="verifyForm.errors.result_prices ? [verifyForm.errors.result_prices] : []"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="verification-note">Catatan Verifikasi</FieldLabel>
|
<FieldLabel for="verification-note">Catatan Verifikasi</FieldLabel>
|
||||||
<Textarea
|
<Textarea id="verification-note" v-model="verifyForm.verification_note"
|
||||||
id="verification-note"
|
placeholder="Contoh: Terdapat 1 barang cacat jahitan saat dihitung di toko" rows="3" />
|
||||||
v-model="verifyForm.verification_note"
|
|
||||||
placeholder="Contoh: Terdapat 1 barang cacat jahitan saat dihitung di toko"
|
|
||||||
rows="3"
|
|
||||||
/>
|
|
||||||
<FieldError
|
<FieldError
|
||||||
:errors="verifyForm.errors.verification_note ? [verifyForm.errors.verification_note] : []"
|
:errors="verifyForm.errors.verification_note ? [verifyForm.errors.verification_note] : []" />
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter class="mt-6">
|
<DialogFooter class="mt-6">
|
||||||
<Button
|
<Button type="button" variant="outline" :disabled="verifyForm.processing"
|
||||||
type="button"
|
@click="verifyDialogOpen = false">
|
||||||
variant="outline"
|
|
||||||
:disabled="verifyForm.processing"
|
|
||||||
@click="verifyDialogOpen = false"
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" :disabled="verifyForm.processing">
|
||||||
type="submit"
|
|
||||||
:disabled="verifyForm.processing"
|
|
||||||
>
|
|
||||||
{{ verifyForm.processing ? 'Menyimpan...' : 'Verifikasi' }}
|
{{ verifyForm.processing ? 'Menyimpan...' : 'Verifikasi' }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import {
|
|||||||
} from '@lucide/vue';
|
} from '@lucide/vue';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import OrderPrintButton from './form/OrderPrintButton.vue';
|
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
@ -36,8 +35,9 @@ import { MarketplaceFeeValueType } from '@/constants/marketplace-fee-value-type'
|
|||||||
import { OrderStatus } from '@/constants/order-status';
|
import { OrderStatus } from '@/constants/order-status';
|
||||||
import { StockQuality } from '@/constants/stock-quality';
|
import { StockQuality } from '@/constants/stock-quality';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { OrderDetail, OrderStatusAction } from '@/types/order';
|
|
||||||
import { index, edit, destroy, transition_status } from '@/routes/admin/manage/orders';
|
import { index, edit, destroy, transition_status } from '@/routes/admin/manage/orders';
|
||||||
|
import type { OrderDetail, OrderStatusAction } from '@/types/order';
|
||||||
|
import OrderPrintButton from './form/OrderPrintButton.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
order: OrderDetail;
|
order: OrderDetail;
|
||||||
@ -59,15 +59,30 @@ const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyO
|
|||||||
});
|
});
|
||||||
|
|
||||||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||||
if (status === OrderStatus.COMPLETED) return 'default';
|
if (status === OrderStatus.COMPLETED) {
|
||||||
if (status === OrderStatus.CANCELLED) return 'destructive';
|
return 'default';
|
||||||
if (status === OrderStatus.PROCESSING) return 'secondary';
|
}
|
||||||
|
|
||||||
|
if (status === OrderStatus.CANCELLED) {
|
||||||
|
return 'destructive';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === OrderStatus.PROCESSING) {
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
return 'outline';
|
return 'outline';
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionIcon(status: string) {
|
function actionIcon(status: string) {
|
||||||
if (status === OrderStatus.PROCESSING) return Send;
|
if (status === OrderStatus.PROCESSING) {
|
||||||
if (status === OrderStatus.COMPLETED) return Check;
|
return Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === OrderStatus.COMPLETED) {
|
||||||
|
return Check;
|
||||||
|
}
|
||||||
|
|
||||||
return X;
|
return X;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,9 +94,11 @@ function statusConfirmDescription(action: OrderStatusAction): string {
|
|||||||
if (action.status === OrderStatus.PROCESSING) {
|
if (action.status === OrderStatus.PROCESSING) {
|
||||||
return `Pesanan ${props.order.order_number} akan dikirim dan diproses.`;
|
return `Pesanan ${props.order.order_number} akan dikirim dan diproses.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.status === OrderStatus.COMPLETED) {
|
if (action.status === OrderStatus.COMPLETED) {
|
||||||
return `Pesanan ${props.order.order_number} akan ditandai selesai.`;
|
return `Pesanan ${props.order.order_number} akan ditandai selesai.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `Pesanan ${props.order.order_number} akan dibatalkan. Stok produk akan dikembalikan.`;
|
return `Pesanan ${props.order.order_number} akan dibatalkan. Stok produk akan dikembalikan.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +108,9 @@ function openStatusConfirm(action: OrderStatusAction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transitionStatus() {
|
function transitionStatus() {
|
||||||
if (!pendingAction.value) return;
|
if (!pendingAction.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
statusProcessing.value = true;
|
statusProcessing.value = true;
|
||||||
|
|
||||||
@ -102,9 +121,10 @@ function transitionStatus() {
|
|||||||
statusConfirmOpen.value = false;
|
statusConfirmOpen.value = false;
|
||||||
pendingAction.value = null;
|
pendingAction.value = null;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
toast.error(typeof message === 'string' ? message : 'Gagal memperbarui status pesanan.');
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
statusProcessing.value = false;
|
statusProcessing.value = false;
|
||||||
@ -176,7 +196,9 @@ function formatFeeRule(fee: { scope: string; value_type: string; value: number }
|
|||||||
}
|
}
|
||||||
|
|
||||||
const feeEntries = computed(() => {
|
const feeEntries = computed(() => {
|
||||||
if (!snapshot.value) return [];
|
if (!snapshot.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return Object.entries(snapshot.value.fees)
|
return Object.entries(snapshot.value.fees)
|
||||||
.filter(([, rule]) => rule.value > 0)
|
.filter(([, rule]) => rule.value > 0)
|
||||||
@ -191,6 +213,7 @@ const feeEntries = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head :title="`Detail Pesanan ${order.order_number}`" />
|
<Head :title="`Detail Pesanan ${order.order_number}`" />
|
||||||
|
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
@ -208,13 +231,10 @@ const feeEntries = computed(() => {
|
|||||||
<OrderPrintButton :order="order as any" />
|
<OrderPrintButton :order="order as any" />
|
||||||
|
|
||||||
<template v-for="action in order.available_actions" :key="action.status">
|
<template v-for="action in order.available_actions" :key="action.status">
|
||||||
<Button
|
<Button v-if="canPerformAction(action)" size="sm"
|
||||||
v-if="canPerformAction(action)"
|
|
||||||
size="sm"
|
|
||||||
:variant="action.destructive ? 'outline' : 'default'"
|
:variant="action.destructive ? 'outline' : 'default'"
|
||||||
:class="action.destructive ? 'text-destructive hover:text-destructive' : ''"
|
:class="action.destructive ? 'text-destructive hover:text-destructive' : ''"
|
||||||
@click="openStatusConfirm(action)"
|
@click="openStatusConfirm(action)">
|
||||||
>
|
|
||||||
<component :is="actionIcon(action.status)" class="size-3.5" />
|
<component :is="actionIcon(action.status)" class="size-3.5" />
|
||||||
{{ action.label }}
|
{{ action.label }}
|
||||||
</Button>
|
</Button>
|
||||||
@ -227,13 +247,8 @@ const feeEntries = computed(() => {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button v-if="canDelete" variant="outline" size="sm" class="text-destructive hover:text-destructive"
|
||||||
v-if="canDelete"
|
@click="deleteConfirmOpen = true">
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="text-destructive hover:text-destructive"
|
|
||||||
@click="deleteConfirmOpen = true"
|
|
||||||
>
|
|
||||||
<Trash2 class="size-3.5" />
|
<Trash2 class="size-3.5" />
|
||||||
Hapus
|
Hapus
|
||||||
</Button>
|
</Button>
|
||||||
@ -420,7 +435,8 @@ const feeEntries = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-muted-foreground">Total Potongan</span>
|
<span class="text-muted-foreground">Total Potongan</span>
|
||||||
<span class="tabular-nums text-destructive">-{{ formatRp(snapshot.total_fee_amount) }}</span>
|
<span class="tabular-nums text-destructive">-{{ formatRp(snapshot.total_fee_amount)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@ -481,11 +497,8 @@ const feeEntries = computed(() => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div v-for="row in summaryRows" :key="row.label"
|
||||||
v-for="row in summaryRows"
|
class="flex items-center justify-between text-sm">
|
||||||
:key="row.label"
|
|
||||||
class="flex items-center justify-between text-sm"
|
|
||||||
>
|
|
||||||
<span class="text-muted-foreground">{{ row.label }}</span>
|
<span class="text-muted-foreground">{{ row.label }}</span>
|
||||||
<span class="tabular-nums">{{ row.value }}</span>
|
<span class="tabular-nums">{{ row.value }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -533,26 +546,13 @@ const feeEntries = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog v-model:open="statusConfirmOpen"
|
||||||
v-model:open="statusConfirmOpen"
|
|
||||||
:title="pendingAction ? `${pendingAction.label} pesanan?` : 'Ubah status pesanan?'"
|
:title="pendingAction ? `${pendingAction.label} pesanan?` : 'Ubah status pesanan?'"
|
||||||
:description="pendingAction ? statusConfirmDescription(pendingAction) : ''"
|
:description="pendingAction ? statusConfirmDescription(pendingAction) : ''"
|
||||||
:confirm-label="pendingAction?.label ?? 'Konfirmasi'"
|
:confirm-label="pendingAction?.label ?? 'Konfirmasi'" cancel-label="Batal"
|
||||||
cancel-label="Batal"
|
:destructive="pendingAction?.destructive ?? false" :loading="statusProcessing" @confirm="transitionStatus" />
|
||||||
:destructive="pendingAction?.destructive ?? false"
|
|
||||||
:loading="statusProcessing"
|
|
||||||
@confirm="transitionStatus"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog v-if="canDelete" v-model:open="deleteConfirmOpen" title="Hapus pesanan?"
|
||||||
v-if="canDelete"
|
|
||||||
v-model:open="deleteConfirmOpen"
|
|
||||||
title="Hapus pesanan?"
|
|
||||||
:description="`Pesanan ${order.order_number} akan dihapus.${order.is_editable ? ' Stok produk akan dikembalikan.' : ''}`"
|
:description="`Pesanan ${order.order_number} akan dihapus.${order.is_editable ? ' Stok produk akan dikembalikan.' : ''}`"
|
||||||
confirm-label="Hapus"
|
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyOrder" />
|
||||||
cancel-label="Batal"
|
|
||||||
destructive
|
|
||||||
:loading="deleteProcessing"
|
|
||||||
@confirm="destroyOrder"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -48,6 +48,7 @@ import { apiFetch } from '@/lib/api';
|
|||||||
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
||||||
|
import { store as syncDraftRoute, resync_prices as resyncPricesRoute, destroy as destroyDraftRoute } from '@/routes/admin/manage/orders/draft_items';
|
||||||
import type { Auth } from '@/types/auth';
|
import type { Auth } from '@/types/auth';
|
||||||
import { STOCK_QUALITY_OPTIONS } from '@/types/order';
|
import { STOCK_QUALITY_OPTIONS } from '@/types/order';
|
||||||
import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order';
|
import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order';
|
||||||
@ -55,7 +56,6 @@ import type { ProductPriceItem, ProductVariantItem } from '@/types/product';
|
|||||||
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
|
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
|
||||||
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
|
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
|
||||||
import CustomerQuickCreateModal from './CustomerQuickCreateModal.vue';
|
import CustomerQuickCreateModal from './CustomerQuickCreateModal.vue';
|
||||||
import { store as syncDraftRoute, resync_prices as resyncPricesRoute, destroy as destroyDraftRoute } from '@/routes/admin/manage/orders/draft_items';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
customers: SelectOption[];
|
customers: SelectOption[];
|
||||||
@ -490,9 +490,10 @@ function submit() {
|
|||||||
|
|
||||||
form.transform(() => payload).post(props.submitUrl, {
|
form.transform(() => payload).post(props.submitUrl, {
|
||||||
forceFormData: true,
|
forceFormData: true,
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const firstError = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
toast.error(firstError || 'Gagal menyimpan data. Periksa kembali formulir.');
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -769,7 +770,8 @@ function submit() {
|
|||||||
<p class="truncate text-xs text-muted-foreground">
|
<p class="truncate text-xs text-muted-foreground">
|
||||||
{{ item.variant_name }}
|
{{ item.variant_name }}
|
||||||
·
|
·
|
||||||
{{ item.stock_quality_label ?? (item.stock_quality === StockQuality.REJECT
|
{{ item.stock_quality_label ?? (item.stock_quality ===
|
||||||
|
StockQuality.REJECT
|
||||||
? 'Reject' : 'Bagus') }}
|
? 'Reject' : 'Bagus') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -849,7 +851,8 @@ function submit() {
|
|||||||
<div v-if="printAfterSave" class="mt-2 flex items-center gap-2 pl-1">
|
<div v-if="printAfterSave" class="mt-2 flex items-center gap-2 pl-1">
|
||||||
<span class="text-xs text-muted-foreground">Ukuran kertas:</span>
|
<span class="text-xs text-muted-foreground">Ukuran kertas:</span>
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
<button v-for="size in ([PaperSize.MM_58, PaperSize.MM_80] as const)" :key="size" type="button"
|
<button v-for="size in ([PaperSize.MM_58, PaperSize.MM_80] as const)"
|
||||||
|
:key="size" type="button"
|
||||||
class="inline-flex h-7 items-center rounded-md border px-2.5 text-xs font-medium transition-colors cursor-pointer"
|
class="inline-flex h-7 items-center rounded-md border px-2.5 text-xs font-medium transition-colors cursor-pointer"
|
||||||
:class="selectedPaperSize === size
|
:class="selectedPaperSize === size
|
||||||
? 'border-primary bg-primary/5 text-primary'
|
? 'border-primary bg-primary/5 text-primary'
|
||||||
@ -887,7 +890,8 @@ function submit() {
|
|||||||
<p class="truncate text-sm font-medium">{{ item.product_name }}</p>
|
<p class="truncate text-sm font-medium">{{ item.product_name }}</p>
|
||||||
<p class="truncate text-xs text-muted-foreground">
|
<p class="truncate text-xs text-muted-foreground">
|
||||||
{{ item.variant_name }}
|
{{ item.variant_name }}
|
||||||
· {{ item.stock_quality_label ?? (item.stock_quality === StockQuality.REJECT ? 'Reject' : 'Bagus')
|
· {{ item.stock_quality_label ?? (item.stock_quality === StockQuality.REJECT ? 'Reject'
|
||||||
|
: 'Bagus')
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import { router } from '@inertiajs/vue3';
|
|||||||
import { Check, Send, X } from '@lucide/vue';
|
import { Check, Send, X } from '@lucide/vue';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import OrderPrintButton from '../form/OrderPrintButton.vue';
|
|
||||||
import { RowDeleteAction, RowDetailAction, RowEditAction, RowStatusAction } from '@/components/button';
|
import { RowDeleteAction, RowDetailAction, RowEditAction, RowStatusAction } from '@/components/button';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import { OrderStatus } from '@/constants/order-status';
|
import { OrderStatus } from '@/constants/order-status';
|
||||||
import type { OrderListItem, OrderStatusAction } from '@/types/order';
|
|
||||||
import { show, edit, destroy, transition_status } from '@/routes/admin/manage/orders';
|
import { show, edit, destroy, transition_status } from '@/routes/admin/manage/orders';
|
||||||
|
import type { OrderListItem, OrderStatusAction } from '@/types/order';
|
||||||
|
import OrderPrintButton from '../form/OrderPrintButton.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
order: OrderListItem;
|
order: OrderListItem;
|
||||||
@ -63,10 +63,10 @@ function transitionStatus() {
|
|||||||
statusConfirmOpen.value = false;
|
statusConfirmOpen.value = false;
|
||||||
pendingAction.value = null;
|
pendingAction.value = null;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
toast.error(typeof message === 'string' ? message : 'Gagal memperbarui status pesanan.');
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
statusProcessing.value = false;
|
statusProcessing.value = false;
|
||||||
@ -92,17 +92,12 @@ function actionIcon(status: string) {
|
|||||||
<OrderPrintButton :order="order" />
|
<OrderPrintButton :order="order" />
|
||||||
|
|
||||||
<template v-for="action in availableActions" :key="action.status">
|
<template v-for="action in availableActions" :key="action.status">
|
||||||
<RowStatusAction v-if="action.icon_only && canPerformAction(action)"
|
<RowStatusAction v-if="action.icon_only && canPerformAction(action)" :icon="actionIcon(action.status)"
|
||||||
:icon="actionIcon(action.status)" :label="action.label" :destructive="action.destructive"
|
:label="action.label" :destructive="action.destructive" @click="openStatusConfirm(action)" />
|
||||||
@click="openStatusConfirm(action)" />
|
|
||||||
|
|
||||||
<Button
|
<Button v-else-if="canPerformAction(action)" size="sm" :variant="action.destructive ? 'outline' : 'default'"
|
||||||
v-else-if="canPerformAction(action)"
|
|
||||||
size="sm"
|
|
||||||
:variant="action.destructive ? 'outline' : 'default'"
|
|
||||||
:class="action.destructive ? 'text-destructive hover:text-destructive' : ''"
|
:class="action.destructive ? 'text-destructive hover:text-destructive' : ''"
|
||||||
@click="openStatusConfirm(action)"
|
@click="openStatusConfirm(action)">
|
||||||
>
|
|
||||||
<component :is="actionIcon(action.status)" class="size-3.5" />
|
<component :is="actionIcon(action.status)" class="size-3.5" />
|
||||||
{{ action.label }}
|
{{ action.label }}
|
||||||
</Button>
|
</Button>
|
||||||
@ -119,14 +114,9 @@ function actionIcon(status: string) {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog v-model:open="statusConfirmOpen"
|
||||||
v-model:open="statusConfirmOpen"
|
|
||||||
:title="pendingAction ? `${pendingAction.label} pesanan?` : 'Ubah status pesanan?'"
|
:title="pendingAction ? `${pendingAction.label} pesanan?` : 'Ubah status pesanan?'"
|
||||||
:description="pendingAction ? statusConfirmDescription(pendingAction) : ''"
|
:description="pendingAction ? statusConfirmDescription(pendingAction) : ''"
|
||||||
:confirm-label="pendingAction?.label ?? 'Konfirmasi'"
|
:confirm-label="pendingAction?.label ?? 'Konfirmasi'" cancel-label="Batal"
|
||||||
cancel-label="Batal"
|
:destructive="pendingAction?.destructive ?? false" :loading="statusProcessing" @confirm="transitionStatus" />
|
||||||
:destructive="pendingAction?.destructive ?? false"
|
|
||||||
:loading="statusProcessing"
|
|
||||||
@confirm="transitionStatus"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { router, useForm } from '@inertiajs/vue3';
|
import { useForm } from '@inertiajs/vue3';
|
||||||
import { X } from '@lucide/vue';
|
import { X } from '@lucide/vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
@ -19,8 +19,8 @@ import {
|
|||||||
} from '@/components/ui/field';
|
} from '@/components/ui/field';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { CuttingListItem } from '@/types/cutting';
|
|
||||||
import { approve, reject } from '@/routes/admin/manage/owner_verifications';
|
import { approve, reject } from '@/routes/admin/manage/owner_verifications';
|
||||||
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cutting: CuttingListItem;
|
cutting: CuttingListItem;
|
||||||
@ -39,27 +39,29 @@ const rejectForm = useForm({
|
|||||||
function submitApprove() {
|
function submitApprove() {
|
||||||
approveForm
|
approveForm
|
||||||
.post(approve.url(props.cutting.id), {
|
.post(approve.url(props.cutting.id), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
toast.error(message ?? 'Gagal menyetujui verifikasi.');
|
toast.error(errors.system);
|
||||||
},
|
}
|
||||||
});
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitReject() {
|
function submitReject() {
|
||||||
rejectForm
|
rejectForm
|
||||||
.post(reject.url(props.cutting.id), {
|
.post(reject.url(props.cutting.id), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
rejectDialogOpen.value = false;
|
rejectDialogOpen.value = false;
|
||||||
rejectForm.reset();
|
rejectForm.reset();
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
toast.error(message ?? 'Gagal menolak verifikasi.');
|
toast.error(errors.system);
|
||||||
},
|
}
|
||||||
});
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -91,32 +93,17 @@ function submitReject() {
|
|||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="reject-reason">Alasan Penolakan</FieldLabel>
|
<FieldLabel for="reject-reason">Alasan Penolakan</FieldLabel>
|
||||||
<Textarea
|
<Textarea id="reject-reason" v-model="rejectForm.reason" placeholder="Jelaskan alasan penolakan..."
|
||||||
id="reject-reason"
|
rows="3" />
|
||||||
v-model="rejectForm.reason"
|
<FieldError :errors="rejectForm.errors.reason ? [rejectForm.errors.reason] : []" />
|
||||||
placeholder="Jelaskan alasan penolakan..."
|
|
||||||
rows="3"
|
|
||||||
/>
|
|
||||||
<FieldError
|
|
||||||
:errors="rejectForm.errors.reason ? [rejectForm.errors.reason] : []"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button type="button" variant="outline" @click="rejectDialogOpen = false">
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
@click="rejectDialogOpen = false"
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="button" variant="destructive" :disabled="rejectForm.processing" @click="submitReject">
|
||||||
type="button"
|
|
||||||
variant="destructive"
|
|
||||||
:disabled="rejectForm.processing"
|
|
||||||
@click="submitReject"
|
|
||||||
>
|
|
||||||
<X class="size-4" />
|
<X class="size-4" />
|
||||||
Tolak
|
Tolak
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -44,12 +44,12 @@ import { getFirstCoverImage } from '@/lib/catalog-cover';
|
|||||||
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 { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
||||||
|
import { store as syncDraftRoute, destroy as destroyDraftRoute } from '@/routes/admin/manage/purchases/draft_items';
|
||||||
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
|
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
|
||||||
import type { MediaItem, MediaUploadState } from '@/types/media';
|
import type { MediaItem, MediaUploadState } from '@/types/media';
|
||||||
import type { PurchaseCartItem, PurchaseCatalogItem, SelectOption } from '@/types/purchase';
|
import type { PurchaseCartItem, PurchaseCatalogItem, SelectOption } from '@/types/purchase';
|
||||||
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
|
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
|
||||||
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
|
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
|
||||||
import { store as syncDraftRoute, destroy as destroyDraftRoute } from '@/routes/admin/manage/purchases/draft_items';
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
suppliers: SelectOption[];
|
suppliers: SelectOption[];
|
||||||
catalog: PurchaseCatalogItem[];
|
catalog: PurchaseCatalogItem[];
|
||||||
@ -323,8 +323,10 @@ function submit() {
|
|||||||
|
|
||||||
form.transform(() => payload).post(props.submitUrl, {
|
form.transform(() => payload).post(props.submitUrl, {
|
||||||
forceFormData: true,
|
forceFormData: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { router, useForm } from '@inertiajs/vue3';
|
import { useForm } from '@inertiajs/vue3';
|
||||||
import { Check } from '@lucide/vue';
|
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { RowApproveAction } from '@/components/button';
|
import { RowApproveAction } from '@/components/button';
|
||||||
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -22,20 +22,14 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip';
|
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
|
||||||
import { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
|
import { verify } from '@/routes/admin/manage/stocks';
|
||||||
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
import {
|
import {
|
||||||
PRICE_TYPES,
|
PRICE_TYPES,
|
||||||
PRICE_TYPE_LABELS,
|
PRICE_TYPE_LABELS,
|
||||||
} from '@/types/product';
|
} from '@/types/product';
|
||||||
import type { CuttingListItem } from '@/types/cutting';
|
|
||||||
import { verify } from '@/routes/admin/manage/stocks';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cutting: CuttingListItem;
|
cutting: CuttingListItem;
|
||||||
@ -148,20 +142,16 @@ function submitVerify() {
|
|||||||
result_prices: buildResultPricesPayload(),
|
result_prices: buildResultPricesPayload(),
|
||||||
}))
|
}))
|
||||||
.post(verify.url(props.cutting.id), {
|
.post(verify.url(props.cutting.id), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
verifyDialogOpen.value = false;
|
verifyDialogOpen.value = false;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors: any) => {
|
||||||
const message = Object.values(errors)[0];
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
toast.error(
|
}
|
||||||
typeof message === 'string'
|
},
|
||||||
? message
|
});
|
||||||
: 'Gagal memverifikasi cutting.',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -178,55 +168,44 @@ function submitVerify() {
|
|||||||
<form @submit.prevent="submitVerify">
|
<form @submit.prevent="submitVerify">
|
||||||
<div class="space-y-4 max-h-[60vh] overflow-y-auto scrollbar-thin px-1 py-1">
|
<div class="space-y-4 max-h-[60vh] overflow-y-auto scrollbar-thin px-1 py-1">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
Verifikasi jumlah produk yang diterima dan tentukan harga jual. Stok akan ditambahkan setelah verifikasi.
|
Verifikasi jumlah produk yang diterima dan tentukan harga jual. Stok akan ditambahkan setelah
|
||||||
|
verifikasi.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex items-center justify-between rounded-lg border p-3">
|
<div class="flex items-center justify-between rounded-lg border p-3">
|
||||||
<Label for="stock-all-matches" class="text-sm font-medium">
|
<Label for="stock-all-matches" class="text-sm font-medium">
|
||||||
Semua sesuai dengan data cutting
|
Semua sesuai dengan data cutting
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch id="stock-all-matches" :model-value="allMatches" @update:model-value="setAllMatches" />
|
||||||
id="stock-all-matches"
|
|
||||||
:model-value="allMatches"
|
|
||||||
@update:model-value="setAllMatches"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id" class="flex items-center gap-3 rounded-lg border p-3">
|
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id"
|
||||||
|
class="flex items-center gap-3 rounded-lg border p-3">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="font-medium text-sm truncate">{{ result.name }}</p>
|
<p class="font-medium text-sm truncate">{{ result.name }}</p>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
{{ result.cutting_result }} pcs
|
{{ result.cutting_result }} pcs
|
||||||
<span class="text-[10px]">({{ result.original_warehouse_stock }} bagus, {{ result.original_cutting_reject }} reject)</span>
|
<span class="text-[10px]">({{ result.original_warehouse_stock }} bagus, {{
|
||||||
|
result.original_cutting_reject }} reject)</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<label :for="`stock-good-${index}`" class="text-[10px] text-muted-foreground block">Bagus</label>
|
<label :for="`stock-good-${index}`"
|
||||||
<Input
|
class="text-[10px] text-muted-foreground block">Bagus</label>
|
||||||
:id="`stock-good-${index}`"
|
<Input :id="`stock-good-${index}`" type="number"
|
||||||
type="number"
|
v-model.number="result.warehouse_stock" min="0" :max="result.cutting_result"
|
||||||
v-model.number="result.warehouse_stock"
|
class="h-7 w-16 px-1.5 text-xs text-center" :disabled="allMatches"
|
||||||
min="0"
|
@input="result.cutting_reject = result.cutting_result - result.warehouse_stock" />
|
||||||
:max="result.cutting_result"
|
|
||||||
class="h-7 w-16 px-1.5 text-xs text-center"
|
|
||||||
:disabled="allMatches"
|
|
||||||
@input="result.cutting_reject = result.cutting_result - result.warehouse_stock"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<label :for="`stock-reject-${index}`" class="text-[10px] text-muted-foreground block">Reject</label>
|
<label :for="`stock-reject-${index}`"
|
||||||
<Input
|
class="text-[10px] text-muted-foreground block">Reject</label>
|
||||||
:id="`stock-reject-${index}`"
|
<Input :id="`stock-reject-${index}`" type="number"
|
||||||
type="number"
|
v-model.number="result.cutting_reject" min="0" :max="result.cutting_result"
|
||||||
v-model.number="result.cutting_reject"
|
class="h-7 w-16 px-1.5 text-xs text-center" :disabled="allMatches"
|
||||||
min="0"
|
@input="result.warehouse_stock = result.cutting_result - result.cutting_reject" />
|
||||||
:max="result.cutting_result"
|
|
||||||
class="h-7 w-16 px-1.5 text-xs text-center"
|
|
||||||
:disabled="allMatches"
|
|
||||||
@input="result.warehouse_stock = result.cutting_result - result.cutting_reject"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -241,46 +220,29 @@ function submitVerify() {
|
|||||||
<FieldLabel class="text-xs" :for="`shared-price-${type}`">
|
<FieldLabel class="text-xs" :for="`shared-price-${type}`">
|
||||||
{{ PRICE_TYPE_LABELS[type] }}
|
{{ PRICE_TYPE_LABELS[type] }}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<RupiahInput
|
<RupiahInput :id="`shared-price-${type}`" :model-value="verifyForm.shared_prices[type]"
|
||||||
:id="`shared-price-${type}`"
|
@update:model-value="setSharedPrice(type, $event)" />
|
||||||
:model-value="verifyForm.shared_prices[type]"
|
|
||||||
@update:model-value="setSharedPrice(type, $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FieldError
|
<FieldError :errors="verifyForm.errors.result_prices ? [verifyForm.errors.result_prices] : []" />
|
||||||
:errors="verifyForm.errors.result_prices ? [verifyForm.errors.result_prices] : []"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="stock-verification-note">Catatan Verifikasi</FieldLabel>
|
<FieldLabel for="stock-verification-note">Catatan Verifikasi</FieldLabel>
|
||||||
<Textarea
|
<Textarea id="stock-verification-note" v-model="verifyForm.verification_note"
|
||||||
id="stock-verification-note"
|
placeholder="Contoh: Terdapat 1 barang cacat jahitan saat dihitung di toko" rows="3" />
|
||||||
v-model="verifyForm.verification_note"
|
|
||||||
placeholder="Contoh: Terdapat 1 barang cacat jahitan saat dihitung di toko"
|
|
||||||
rows="3"
|
|
||||||
/>
|
|
||||||
<FieldError
|
<FieldError
|
||||||
:errors="verifyForm.errors.verification_note ? [verifyForm.errors.verification_note] : []"
|
:errors="verifyForm.errors.verification_note ? [verifyForm.errors.verification_note] : []" />
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter class="mt-6">
|
<DialogFooter class="mt-6">
|
||||||
<Button
|
<Button type="button" variant="outline" :disabled="verifyForm.processing"
|
||||||
type="button"
|
@click="verifyDialogOpen = false">
|
||||||
variant="outline"
|
|
||||||
:disabled="verifyForm.processing"
|
|
||||||
@click="verifyDialogOpen = false"
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" :disabled="verifyForm.processing">
|
||||||
type="submit"
|
|
||||||
:disabled="verifyForm.processing"
|
|
||||||
>
|
|
||||||
{{ verifyForm.processing ? 'Menyimpan...' : 'Verifikasi & Tambah Stok' }}
|
{{ verifyForm.processing ? 'Menyimpan...' : 'Verifikasi & Tambah Stok' }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -22,8 +22,8 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { useFormDialog } from '@/composables/useFormDialog';
|
import { useFormDialog } from '@/composables/useFormDialog';
|
||||||
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 { CategoryFormData, CategoryListItem } from '@/types/category';
|
|
||||||
import { store, update } from '@/routes/admin/master/categories';
|
import { store, update } from '@/routes/admin/master/categories';
|
||||||
|
import type { CategoryFormData, CategoryListItem } from '@/types/category';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -65,8 +65,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -24,8 +24,8 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { useFormDialog } from '@/composables/useFormDialog';
|
import { useFormDialog } from '@/composables/useFormDialog';
|
||||||
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 { CustomerFormData, CustomerListItem } from '@/types/customer';
|
|
||||||
import { store, update } from '@/routes/admin/master/customers';
|
import { store, update } from '@/routes/admin/master/customers';
|
||||||
|
import type { CustomerFormData, CustomerListItem } from '@/types/customer';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -71,8 +71,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -131,8 +131,10 @@ function buildFormData(): FormData {
|
|||||||
function submit() {
|
function submit() {
|
||||||
const options = {
|
const options = {
|
||||||
forceFormData: true,
|
forceFormData: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { toast } from 'vue-sonner';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { ProductListItem } from '@/types/product';
|
|
||||||
import { toggle_status } from '@/routes/admin/master/products';
|
import { toggle_status } from '@/routes/admin/master/products';
|
||||||
|
import type { ProductListItem } from '@/types/product';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
product: ProductListItem;
|
product: ProductListItem;
|
||||||
@ -37,9 +37,12 @@ function toggleStatus(checked: boolean) {
|
|||||||
is_active: checked,
|
is_active: checked,
|
||||||
}, {
|
}, {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
isActive.value = previous;
|
isActive.value = previous;
|
||||||
toast.error('Gagal memperbarui status produk.');
|
|
||||||
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
processing.value = false;
|
processing.value = false;
|
||||||
|
|||||||
@ -187,8 +187,10 @@ function buildFormData(): FormData {
|
|||||||
function submit() {
|
function submit() {
|
||||||
const options = {
|
const options = {
|
||||||
forceFormData: true,
|
forceFormData: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { toast } from 'vue-sonner';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { RawMaterialListItem } from '@/types/raw-material';
|
|
||||||
import { toggle_status } from '@/routes/admin/master/raw_materials';
|
import { toggle_status } from '@/routes/admin/master/raw_materials';
|
||||||
|
import type { RawMaterialListItem } from '@/types/raw-material';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
material: RawMaterialListItem;
|
material: RawMaterialListItem;
|
||||||
@ -37,9 +37,12 @@ function toggleStatus(checked: boolean) {
|
|||||||
is_active: checked,
|
is_active: checked,
|
||||||
}, {
|
}, {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
isActive.value = previous;
|
isActive.value = previous;
|
||||||
toast.error('Gagal memperbarui status bahan baku.');
|
|
||||||
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
processing.value = false;
|
processing.value = false;
|
||||||
|
|||||||
@ -24,8 +24,8 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { useFormDialog } from '@/composables/useFormDialog';
|
import { useFormDialog } from '@/composables/useFormDialog';
|
||||||
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 { SupplierFormData, SupplierListItem } from '@/types/supplier';
|
|
||||||
import { store, update } from '@/routes/admin/master/suppliers';
|
import { store, update } from '@/routes/admin/master/suppliers';
|
||||||
|
import type { SupplierFormData, SupplierListItem } from '@/types/supplier';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
@ -71,8 +71,10 @@ function submit() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -98,8 +100,8 @@ function submit() {
|
|||||||
<FieldSet class="grid gap-4">
|
<FieldSet class="grid gap-4">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="supplier-name" required>Nama</FieldLabel>
|
<FieldLabel for="supplier-name" required>Nama</FieldLabel>
|
||||||
<Input id="supplier-name" v-model="form.name" type="text" placeholder="Contoh: PT Sumber Makmur"
|
<Input id="supplier-name" v-model="form.name" type="text"
|
||||||
autofocus :maxlength="FIELD_LIMITS.name" />
|
placeholder="Contoh: PT Sumber Makmur" autofocus :maxlength="FIELD_LIMITS.name" />
|
||||||
<FieldError :errors="formErrors(form, 'name')" />
|
<FieldError :errors="formErrors(form, 'name')" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
|
|||||||
@ -96,8 +96,10 @@ function toggleGroupAll(groupName: string, checked: boolean) {
|
|||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
form[props.method](props.submitUrl, {
|
form[props.method](props.submitUrl, {
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -164,7 +166,7 @@ function submit() {
|
|||||||
" />
|
" />
|
||||||
<span class="text-sm">{{
|
<span class="text-sm">{{
|
||||||
perm.label
|
perm.label
|
||||||
}}</span>
|
}}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,10 +6,10 @@ import { toast } from 'vue-sonner';
|
|||||||
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
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 { update } from '@/routes/admin/system/settings/homepage';
|
||||||
import { createMediaUploadState } from '@/types/media';
|
import { createMediaUploadState } from '@/types/media';
|
||||||
import type { MediaUploadState } from '@/types/media';
|
import type { MediaUploadState } from '@/types/media';
|
||||||
import type { HomepageSettingsData } from '@/types/setting';
|
import type { HomepageSettingsData } from '@/types/setting';
|
||||||
import { update } from '@/routes/admin/system/settings/homepage';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: HomepageSettingsData;
|
data: HomepageSettingsData;
|
||||||
@ -49,8 +49,10 @@ function submit() {
|
|||||||
heroImage.value = createMediaUploadState();
|
heroImage.value = createMediaUploadState();
|
||||||
aboutImage.value = createMediaUploadState();
|
aboutImage.value = createMediaUploadState();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan pengaturan homepage.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
@ -68,8 +70,7 @@ function submit() {
|
|||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<MediaDropzone id="hero_image" label="Foto Hero" :max-files="1" v-model="heroImage" />
|
<MediaDropzone id="hero_image" label="Foto Hero" :max-files="1" v-model="heroImage" />
|
||||||
|
|
||||||
<MediaDropzone id="about_image" label="Foto Tentang Kami" :max-files="1"
|
<MediaDropzone id="about_image" label="Foto Tentang Kami" :max-files="1" v-model="aboutImage" />
|
||||||
v-model="aboutImage" />
|
|
||||||
|
|
||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-2">
|
||||||
<MediaDropzone id="gallery_images" label="Koleksi Lookbook" :max-files="10"
|
<MediaDropzone id="gallery_images" label="Koleksi Lookbook" :max-files="10"
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field';
|
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import type { HrSettingsData } from '@/types/setting';
|
|
||||||
import { update } from '@/routes/admin/system/settings/hr';
|
import { update } from '@/routes/admin/system/settings/hr';
|
||||||
|
import type { HrSettingsData } from '@/types/setting';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: HrSettingsData;
|
data: HrSettingsData;
|
||||||
@ -25,8 +25,10 @@ const form = useForm({
|
|||||||
function submit() {
|
function submit() {
|
||||||
form.put(update.url(), {
|
form.put(update.url(), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan pengaturan HR.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,12 +15,12 @@ import {
|
|||||||
import { MarketplacePlatform as MarketplacePlatformConst } from '@/constants/marketplace-platform';
|
import { MarketplacePlatform as MarketplacePlatformConst } from '@/constants/marketplace-platform';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { update } from '@/routes/admin/system/settings/marketplace';
|
||||||
import type {
|
import type {
|
||||||
MarketplacePlatform,
|
MarketplacePlatform,
|
||||||
MarketplaceSettingsData,
|
MarketplaceSettingsData,
|
||||||
} from '@/types/setting';
|
} from '@/types/setting';
|
||||||
import MarketplaceFeeField from './MarketplaceFeeField.vue';
|
import MarketplaceFeeField from './MarketplaceFeeField.vue';
|
||||||
import { update } from '@/routes/admin/system/settings/marketplace';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: MarketplaceSettingsData;
|
data: MarketplaceSettingsData;
|
||||||
@ -67,8 +67,10 @@ function feeErrors(key: string): string[] {
|
|||||||
function submit() {
|
function submit() {
|
||||||
form.put(update.url(), {
|
form.put(update.url(), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan pengaturan marketplace.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field';
|
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import type { SocialMediaSettingsData } from '@/types/setting';
|
|
||||||
import { update } from '@/routes/admin/system/settings/social_media';
|
import { update } from '@/routes/admin/system/settings/social_media';
|
||||||
|
import type { SocialMediaSettingsData } from '@/types/setting';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: SocialMediaSettingsData;
|
data: SocialMediaSettingsData;
|
||||||
@ -23,8 +23,10 @@ const form = useForm({
|
|||||||
function submit() {
|
function submit() {
|
||||||
form.put(update.url(), {
|
form.put(update.url(), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan pengaturan media sosial.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,10 @@ import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/component
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
|
import { update } from '@/routes/admin/system/settings/system';
|
||||||
import { createMediaUploadState } from '@/types/media';
|
import { createMediaUploadState } from '@/types/media';
|
||||||
import type { MediaUploadState } from '@/types/media';
|
import type { MediaUploadState } from '@/types/media';
|
||||||
import type { SystemSettingsData } from '@/types/setting';
|
import type { SystemSettingsData } from '@/types/setting';
|
||||||
import { update } from '@/routes/admin/system/settings/system';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: SystemSettingsData;
|
data: SystemSettingsData;
|
||||||
@ -61,8 +61,10 @@ function submit() {
|
|||||||
|
|
||||||
form.transform(() => payload).put(update.url(), {
|
form.transform(() => payload).put(update.url(), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError: () => {
|
onError: (errors: any) => {
|
||||||
toast.error('Gagal menyimpan pengaturan sistem.');
|
if (errors.system) {
|
||||||
|
toast.error(errors.system);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user