refactor: clean up imports and improve code readability across multiple components
This commit is contained in:
parent
bcf5842c4a
commit
62341ac50a
@ -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'])]
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -74,7 +74,6 @@ export default function PurchaseIndex({
|
||||
pagination,
|
||||
filters,
|
||||
filterWithParams: false,
|
||||
reloadOnly: ['purchases', 'filters'],
|
||||
});
|
||||
|
||||
const selectedSupplier = useMemo(
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user