refactor: clean up imports and improve code readability across multiple components

This commit is contained in:
Yoga Pangestu 2026-08-06 09:55:11 +07:00
parent bcf5842c4a
commit 62341ac50a
13 changed files with 256 additions and 81 deletions

View File

@ -5,7 +5,6 @@
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
#[Guarded(['id'])]

View File

@ -10,7 +10,6 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -38,21 +37,21 @@ protected function casts(): array
protected function email(): Attribute
{
return Attribute::make(
set: fn(string $value) => strtolower($value),
set: fn (string $value) => strtolower($value),
);
}
protected function name(): Attribute
{
return Attribute::make(
get: fn() => $this->userProfile?->full_name ?? '',
get: fn () => $this->userProfile?->full_name ?? '',
);
}
protected function fullName(): Attribute
{
return Attribute::make(
get: fn() => $this->userProfile?->full_name ?: $this->username,
get: fn () => $this->userProfile?->full_name ?: $this->username,
);
}

View File

@ -1,3 +1,29 @@
import { Link, router } from '@inertiajs/react';
import type { LucideIcon } from 'lucide-react';
import {
Activity,
ArrowUpFromLine,
BarChart3,
Boxes,
CalendarCheck,
CalendarDays,
ClipboardCheck,
DollarSign,
HandCoins,
LayoutGrid,
Package,
RefreshCw,
Scissors,
Settings,
Shield,
ShoppingCart,
Tags,
Truck,
UserCircle,
Users,
Wallet,
} from 'lucide-react';
import React from 'react';
import AppLogo from '@/components/app-logo';
import {
Sidebar,
@ -30,32 +56,6 @@ import { index as productsIndex } from '@/routes/admin/master/products';
import { index as rawMaterialsIndex } from '@/routes/admin/master/raw-materials';
import { index as suppliersIndex } from '@/routes/admin/master/suppliers';
import { index as rolesIndex } from '@/routes/admin/settings/roles';
import { Link, router } from '@inertiajs/react';
import type { LucideIcon } from 'lucide-react';
import {
Activity,
ArrowUpFromLine,
BarChart3,
Boxes,
CalendarCheck,
CalendarDays,
ClipboardCheck,
DollarSign,
HandCoins,
LayoutGrid,
Package,
RefreshCw,
Scissors,
Settings,
Shield,
ShoppingCart,
Tags,
Truck,
UserCircle,
Users,
Wallet,
} from 'lucide-react';
import React from 'react';
type NavMenuItem = { title: string; href: string; icon: LucideIcon; permission?: string | string[] };
@ -111,12 +111,20 @@ function MenuGroup({ label, items }: { label: string; items: NavMenuItem[] }) {
const { can, canAny } = useCan();
const filtered = items.filter((item) => {
if (!item.permission) return true;
if (Array.isArray(item.permission)) return canAny(...item.permission);
if (!item.permission) {
return true;
}
if (Array.isArray(item.permission)) {
return canAny(...item.permission);
}
return can(item.permission);
});
if (filtered.length === 0) return null;
if (filtered.length === 0) {
return null;
}
return (
<SidebarGroup>

View File

@ -1,6 +1,6 @@
import { Clock, LogIn, LogOut } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
type TodayAttendance = {
id: number;
@ -29,8 +29,12 @@ export function AttendanceCard({ todayAttendance, isOnLeave, canCheckIn, onCheck
const hasCheckedOut = !!todayAttendance?.check_out_at;
function formatTime(dateStr: string | null): string {
if (!dateStr) return '-';
if (!dateStr) {
return '-';
}
const d = new Date(dateStr);
return d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false });
}

View File

@ -6,8 +6,14 @@ export function useCardTableExpand(
defaultExpanded: boolean | (number | string)[] = false,
) {
const [expandedKeys, setExpandedKeys] = useState<ExpandState>(() => {
if (defaultExpanded === true) return 'all';
if (Array.isArray(defaultExpanded)) return new Set(defaultExpanded);
if (defaultExpanded === true) {
return 'all';
}
if (Array.isArray(defaultExpanded)) {
return new Set(defaultExpanded);
}
return new Set();
});
@ -16,12 +22,15 @@ export function useCardTableExpand(
if (prev === 'all') {
return new Set([key]);
}
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
}, []);
@ -36,7 +45,10 @@ export function useCardTableExpand(
const isExpanded = useCallback(
(key: number | string): boolean => {
if (expandedKeys === 'all') return true;
if (expandedKeys === 'all') {
return true;
}
return expandedKeys.has(key);
},
[expandedKeys],

View File

@ -17,7 +17,10 @@ type PageProps = {
};
function extractNames(items?: RoleOrPermission[]): string[] {
if (!items) return [];
if (!items) {
return [];
}
return items.map((item) => (typeof item === 'string' ? item : item.name));
}
@ -29,24 +32,42 @@ export function useCan() {
const permissionNames = extractNames(user?.permissions);
function can(permission: string): boolean {
if (!user) return false;
if (roleNames.includes('developer') || roleNames.includes('owner')) return true;
if (!user) {
return false;
}
if (roleNames.includes('developer') || roleNames.includes('owner')) {
return true;
}
return permissionNames.includes(permission);
}
function canAny(...permissions: string[]): boolean {
if (!user) return false;
if (roleNames.includes('developer') || roleNames.includes('owner')) return true;
if (!user) {
return false;
}
if (roleNames.includes('developer') || roleNames.includes('owner')) {
return true;
}
return permissions.some((p) => permissionNames.includes(p));
}
function hasRole(role: string): boolean {
if (!user) return false;
if (!user) {
return false;
}
return roleNames.includes(role);
}
function hasAnyRole(roles: string[]): boolean {
if (!user) return false;
if (!user) {
return false;
}
return roles.some((role) => roleNames.includes(role));
}

View File

@ -6,12 +6,15 @@ export function formatRupiahShort(value: number): string {
if (value >= 1_000_000_000) {
return (value / 1_000_000_000).toFixed(1).replace('.0', '') + 'jt';
}
if (value >= 1_000_000) {
return (value / 1_000_000).toFixed(1).replace('.0', '') + 'jt';
}
if (value >= 1_000) {
return (value / 1_000).toFixed(0) + 'rb';
}
return value.toString();
}

View File

@ -1,5 +1,8 @@
'use no memo';
import { Form, Head, Link, usePage } from '@inertiajs/react';
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { ConfirmDialog } from '@/components/confirm-dialog';
import { FileUpload } from '@/components/file-upload';
import { ImagePreviewModal } from '@/components/image-preview-modal';
@ -19,9 +22,6 @@ import { formatNumber } from '@/lib/format';
import { getTemporaryUrl } from '@/lib/upload';
import { formatCurrency } from '@/lib/utils';
import { index as cuttingIndex, store } from '@/routes/admin/manage/cuttings';
import { Form, Head, Link, usePage } from '@inertiajs/react';
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
import { useCallback, useMemo, useRef, useState } from 'react';
import type { CuttingCreateData } from './columns';
type MaterialState = {
@ -63,6 +63,7 @@ export default function CuttingCreate({ data }: Props) {
photo_url: m.photo_url,
}));
}
return [];
});
const [combinations, setCombinations] = useState<CombinationState[]>(() => {
@ -71,6 +72,7 @@ export default function CuttingCreate({ data }: Props) {
material_result: c.material_result ?? 0,
}));
}
return [];
});
const [selectedMaterialName, setSelectedMaterialName] = useState(draft?.selectedMaterialName ?? '');
@ -149,12 +151,21 @@ export default function CuttingCreate({ data }: Props) {
const addVariant = useCallback(
(priceId: number) => {
if (!selectedMaterial) return;
if (!selectedMaterial) {
return;
}
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
if (!price) return;
if (!price) {
return;
}
setMaterials((prev) => {
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) return prev;
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
return prev;
}
return [
...prev,
{
@ -187,21 +198,26 @@ export default function CuttingCreate({ data }: Props) {
}, []);
const confirmCombo = useCallback(() => {
if (comboSelectedPriceIds.length < 2) return;
if (comboSelectedPriceIds.length < 2) {
return;
}
const comboIndex = combinations.length;
const newMaterials: MaterialState[] = comboSelectedPriceIds.map((priceId) => {
let foundMaterial: typeof rawMaterials[number] | undefined;
let foundPrice: typeof rawMaterials[number]['raw_material_prices'][number] | undefined;
for (const rm of rawMaterials) {
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
if (p) {
foundMaterial = rm;
foundPrice = p;
break;
}
}
return {
raw_material_price_id: priceId,
material_usage: 0,
@ -238,6 +254,7 @@ export default function CuttingCreate({ data }: Props) {
setMaterials((prev) => {
const updated = [...prev];
(updated[index] as Record<string, unknown>)[field] = value;
return updated;
});
},
@ -251,6 +268,7 @@ export default function CuttingCreate({ data }: Props) {
const totalMaterialCost = useMemo(() => {
return materials.reduce((sum, m) => {
const price = priceMap.get(m.raw_material_price_id);
return sum + (price ? price.price * m.material_usage : 0);
}, 0);
}, [materials, priceMap]);
@ -297,7 +315,9 @@ export default function CuttingCreate({ data }: Props) {
</Button>
</div>
<Form action={store()} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => { submittingRef.current = true; }}>
<Form action={store()} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
submittingRef.current = true;
}}>
{({ errors, processing }) => (
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-6 md:col-span-2">
@ -337,6 +357,7 @@ export default function CuttingCreate({ data }: Props) {
{selectedMaterial.raw_material_prices.map((price) => {
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
return (
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
<div className="flex min-w-0 items-center gap-3">
@ -435,7 +456,9 @@ export default function CuttingCreate({ data }: Props) {
<div className="grid gap-2">
<Label>Foto</Label>
<FileUpload value={photo} onChange={(key) => { setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null); }} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
<FileUpload value={photo} onChange={(key) => {
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
<InputError message={errors.photo_key} />
</div>
@ -474,7 +497,10 @@ export default function CuttingCreate({ data }: Props) {
materials.forEach((m, i) => {
if (m.combination_id !== null) {
if (!comboMap.has(m.combination_id)) comboMap.set(m.combination_id, []);
if (!comboMap.has(m.combination_id)) {
comboMap.set(m.combination_id, []);
}
comboMap.get(m.combination_id)!.push({ m, index: i });
} else {
singleItems.push({ m, index: i });
@ -509,6 +535,7 @@ export default function CuttingCreate({ data }: Props) {
{group.items.map(({ m, index }) => {
const price = priceMap.get(m.raw_material_price_id);
const cartKey = `material-${index}`;
return (
<div key={cartKey} className="space-y-2">
<div className="flex items-start justify-between gap-2">
@ -571,7 +598,11 @@ export default function CuttingCreate({ data }: Props) {
<ImagePreviewModal
open={previewKey !== null}
onOpenChange={(open) => { if (!open) setPreviewKey(null); }}
onOpenChange={(open) => {
if (!open) {
setPreviewKey(null);
}
}}
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name}${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
@ -619,8 +650,10 @@ export default function CuttingCreate({ data }: Props) {
let variantName = '';
let materialName = '';
let photoUrl: string | null = null;
for (const rm of rawMaterials) {
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
if (p) {
variantName = p.variant;
materialName = rm.name;
@ -628,6 +661,7 @@ export default function CuttingCreate({ data }: Props) {
break;
}
}
return (
<div key={priceId} className="flex items-center justify-between gap-2 rounded-md border border-primary bg-primary/5 px-3 py-2">
<div className="flex min-w-0 items-center gap-3">
@ -657,6 +691,7 @@ export default function CuttingCreate({ data }: Props) {
<div className="space-y-2">
{comboMaterial.raw_material_prices.map((price) => {
const isSelected = comboSelectedPriceIds.includes(price.id);
return (
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
<div className="flex min-w-0 items-center gap-3">
@ -689,11 +724,41 @@ export default function CuttingCreate({ data }: Props) {
</DialogContent>
</Dialog>
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => { if (!open) { setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); } }} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => { if (deleteMaterialIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex)); setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); }} />
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
if (!open) {
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
}
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
if (deleteMaterialIndex !== null) {
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
}
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); } }} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (cartDeleteIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex)); setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); }} />
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
}} />
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); } }} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (comboDeleteIndex !== null) { setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex)); } setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); }} />
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
if (!open) {
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
}
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
if (cartDeleteIndex !== null) {
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
}
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
}} />
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
if (!open) {
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
}
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
if (comboDeleteIndex !== null) {
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
}
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
}} />
</div>
</>
);

View File

@ -1,5 +1,8 @@
'use no memo';
import { Form, Head, Link } from '@inertiajs/react';
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { ConfirmDialog } from '@/components/confirm-dialog';
import { FileUpload } from '@/components/file-upload';
import { ImagePreviewModal } from '@/components/image-preview-modal';
@ -17,9 +20,6 @@ import { formatNumber } from '@/lib/format';
import { getTemporaryUrl } from '@/lib/upload';
import { formatCurrency } from '@/lib/utils';
import { index as cuttingIndex, update } from '@/routes/admin/manage/cuttings';
import { Form, Head, Link } from '@inertiajs/react';
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
import { useCallback, useMemo, useRef, useState } from 'react';
import type { CuttingCreateData, CuttingForEdit } from './columns';
type MaterialState = {
@ -131,12 +131,21 @@ export default function CuttingEdit({ cutting, data }: Props) {
const addVariant = useCallback(
(priceId: number) => {
if (!selectedMaterial) return;
if (!selectedMaterial) {
return;
}
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
if (!price) return;
if (!price) {
return;
}
setMaterials((prev) => {
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) return prev;
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
return prev;
}
return [
...prev,
{
@ -169,21 +178,26 @@ export default function CuttingEdit({ cutting, data }: Props) {
}, []);
const confirmCombo = useCallback(() => {
if (comboSelectedPriceIds.length < 2) return;
if (comboSelectedPriceIds.length < 2) {
return;
}
const comboIndex = combinations.length;
const newMaterials: MaterialState[] = comboSelectedPriceIds.map((priceId) => {
let foundMaterial: (typeof rawMaterials)[number] | undefined;
let foundPrice: (typeof rawMaterials)[number]['raw_material_prices'][number] | undefined;
for (const rm of rawMaterials) {
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
if (p) {
foundMaterial = rm;
foundPrice = p;
break;
}
}
return {
raw_material_price_id: priceId,
material_usage: 0,
@ -215,6 +229,7 @@ export default function CuttingEdit({ cutting, data }: Props) {
setMaterials((prev) => {
const updated = [...prev];
(updated[index] as Record<string, unknown>)[field] = value;
return updated;
});
},
@ -228,6 +243,7 @@ export default function CuttingEdit({ cutting, data }: Props) {
const totalMaterialCost = useMemo(() => {
return materials.reduce((sum, m) => {
const price = priceMap.get(m.raw_material_price_id);
return sum + (price ? price.price * m.material_usage : 0);
}, 0);
}, [materials, priceMap]);
@ -274,7 +290,9 @@ export default function CuttingEdit({ cutting, data }: Props) {
</Button>
</div>
<Form action={update(cutting.id)} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => { submittingRef.current = true; }}>
<Form action={update(cutting.id)} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
submittingRef.current = true;
}}>
{({ errors, processing }) => (
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-6 md:col-span-2">
@ -314,6 +332,7 @@ export default function CuttingEdit({ cutting, data }: Props) {
{selectedMaterial.raw_material_prices.map((price) => {
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
return (
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
<div className="flex min-w-0 items-center gap-3">
@ -410,7 +429,9 @@ export default function CuttingEdit({ cutting, data }: Props) {
<div className="grid gap-2">
<Label>Foto</Label>
<FileUpload value={photo} onChange={(key) => { setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null); }} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
<FileUpload value={photo} onChange={(key) => {
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
<InputError message={errors.photo_key} />
</div>
@ -449,7 +470,10 @@ export default function CuttingEdit({ cutting, data }: Props) {
materials.forEach((m, i) => {
if (m.combination_id !== null) {
if (!comboMap.has(m.combination_id)) comboMap.set(m.combination_id, []);
if (!comboMap.has(m.combination_id)) {
comboMap.set(m.combination_id, []);
}
comboMap.get(m.combination_id)!.push({ m, index: i });
} else {
singleItems.push({ m, index: i });
@ -484,6 +508,7 @@ export default function CuttingEdit({ cutting, data }: Props) {
{group.items.map(({ m, index }) => {
const price = priceMap.get(m.raw_material_price_id);
const cartKey = `material-${index}`;
return (
<div key={cartKey} className="space-y-2">
<div className="flex items-start justify-between gap-2">
@ -546,7 +571,11 @@ export default function CuttingEdit({ cutting, data }: Props) {
<ImagePreviewModal
open={previewKey !== null}
onOpenChange={(open) => { if (!open) setPreviewKey(null); }}
onOpenChange={(open) => {
if (!open) {
setPreviewKey(null);
}
}}
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name}${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
@ -594,8 +623,10 @@ export default function CuttingEdit({ cutting, data }: Props) {
let variantName = '';
let materialName = '';
let photoUrl: string | null = null;
for (const rm of rawMaterials) {
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
if (p) {
variantName = p.variant;
materialName = rm.name;
@ -603,6 +634,7 @@ export default function CuttingEdit({ cutting, data }: Props) {
break;
}
}
return (
<div key={priceId} className="flex items-center justify-between gap-2 rounded-md border border-primary bg-primary/5 px-3 py-2">
<div className="flex min-w-0 items-center gap-3">
@ -632,6 +664,7 @@ export default function CuttingEdit({ cutting, data }: Props) {
<div className="space-y-2">
{comboMaterial.raw_material_prices.map((price) => {
const isSelected = comboSelectedPriceIds.includes(price.id);
return (
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
<div className="flex min-w-0 items-center gap-3">
@ -664,11 +697,41 @@ export default function CuttingEdit({ cutting, data }: Props) {
</DialogContent>
</Dialog>
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => { if (!open) { setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); } }} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => { if (deleteMaterialIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex)); setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); }} />
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
if (!open) {
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
}
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
if (deleteMaterialIndex !== null) {
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
}
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); } }} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (cartDeleteIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex)); setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); }} />
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
}} />
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); } }} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (comboDeleteIndex !== null) { setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex)); } setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); }} />
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
if (!open) {
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
}
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
if (cartDeleteIndex !== null) {
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
}
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
}} />
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
if (!open) {
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
}
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
if (comboDeleteIndex !== null) {
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
}
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
}} />
</div>
</>
);

View File

@ -74,7 +74,6 @@ export default function PurchaseIndex({
pagination,
filters,
filterWithParams: false,
reloadOnly: ['purchases', 'filters'],
});
const selectedSupplier = useMemo(

View File

@ -28,7 +28,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import {
Sheet,
SheetContent,
@ -36,6 +35,7 @@ import {
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { useTransactionDraftSave } from '@/hooks/use-transaction-draft';
import { formatNumber } from '@/lib/format';
@ -182,6 +182,7 @@ export default function TransactionCreate({ data }: Props) {
if (stockType === 'reject') {
return priceTypeOptions.filter((o) => o.value === 'reject');
}
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
}, [stockType, priceTypeOptions]);

View File

@ -28,7 +28,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import {
Sheet,
SheetContent,
@ -36,6 +35,7 @@ import {
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { formatNumber } from '@/lib/format';
import { getTemporaryUrl } from '@/lib/upload';
@ -151,6 +151,7 @@ export default function TransactionEdit({ transaction, data }: Props) {
if (stockType === 'reject') {
return priceTypeOptions.filter((o) => o.value === 'reject');
}
return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value));
}, [stockType, priceTypeOptions]);

View File

@ -1,3 +1,6 @@
import { Head, Link, router } from '@inertiajs/react';
import { Plus } from 'lucide-react';
import { useState } from 'react';
import { CardTable } from '@/components/card-table';
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
import { FilterPopover } from '@/components/filter-popover';
@ -11,6 +14,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useCan } from '@/hooks/use-can';
import { useServerTable } from '@/hooks/use-server-table';
import {
destroy,
@ -22,13 +26,9 @@ import {
import {
destroy as variantDestroy
} from '@/routes/admin/master/raw-materials/variants';
import { Head, Link, router } from '@inertiajs/react';
import { Plus } from 'lucide-react';
import { useState } from 'react';
import type { RawMaterial, RawMaterialVariant } from './columns';
import { RawMaterialCardRow } from './raw-material-card';
import { RawMaterialVariantSubRow } from './variant/sub-row';
import { useCan } from '@/hooks/use-can';
type Props = {
rawMaterials: {