mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into hotfix/adjustment-recording-fifo-stock
This commit is contained in:
+2
-1
@@ -8,7 +8,8 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write .",
|
||||||
|
"pre-commit": "npm run format && npm run lint && npx tsc --noEmit && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.1",
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
|
||||||
|
|
||||||
|
const MasterKandangPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<MasterKandangContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MasterKandangPage;
|
||||||
@@ -314,6 +314,10 @@ const KandangsTable = () => {
|
|||||||
accessorFn: (row) => row.pic?.name ?? '-',
|
accessorFn: (row) => row.pic?.name ?? '-',
|
||||||
header: 'PIC',
|
header: 'PIC',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => row.kandang_group?.name ?? '-',
|
||||||
|
header: 'Kandang Group',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props: CellContext<Kandang, unknown>) => {
|
cell: (props: CellContext<Kandang, unknown>) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
type KandangFormSchemaType = {
|
type KandangFormSchemaType = {
|
||||||
@@ -19,6 +20,7 @@ type KandangFormSchemaType = {
|
|||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
| null;
|
| null;
|
||||||
|
group?: OptionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
||||||
@@ -42,6 +44,11 @@ export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
|||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
|
|
||||||
|
group: Yup.object({
|
||||||
|
value: Yup.number().min(1).required('Kandang Grup wajib diisi!'),
|
||||||
|
label: Yup.string().required('Kandang Grup wajib diisi!'),
|
||||||
|
}).required('Kandang Grup wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateKandangFormSchema = KandangFormSchema;
|
export const UpdateKandangFormSchema = KandangFormSchema;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useFormik } from 'formik';
|
import { getIn, useFormik } from 'formik';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -34,6 +34,8 @@ import NumberInput from '@/components/input/NumberInput';
|
|||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { User } from '@/types/api/api-general';
|
import { User } from '@/types/api/api-general';
|
||||||
|
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
interface KandangFormProps {
|
interface KandangFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -96,6 +98,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
label: initialValues.pic.name,
|
label: initialValues.pic.name,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
group: initialValues?.kandang_group
|
||||||
|
? {
|
||||||
|
value: initialValues.kandang_group.id,
|
||||||
|
label: initialValues.kandang_group.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
|
|
||||||
@@ -111,6 +119,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
location_id: values.locationId!,
|
location_id: values.locationId!,
|
||||||
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
||||||
pic_id: values.picId!,
|
pic_id: values.picId!,
|
||||||
|
group_id: values.group?.value as number,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -162,6 +171,23 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
formik.setFieldValue('picId', (val as OptionType)?.value);
|
formik.setFieldValue('picId', (val as OptionType)?.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kandang Group
|
||||||
|
const {
|
||||||
|
setInputValue: setKandangGroupSelectInputValue,
|
||||||
|
options: kandangGroupOptions,
|
||||||
|
isLoadingOptions: isLoadingKandangGroupOptions,
|
||||||
|
loadMore: loadMoreKandangGroups,
|
||||||
|
} = useSelect<DailyChecklistKandang>(
|
||||||
|
DailyChecklistKandangApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name'
|
||||||
|
);
|
||||||
|
|
||||||
|
const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched('group', true);
|
||||||
|
formik.setFieldValue('group', val);
|
||||||
|
};
|
||||||
|
|
||||||
const deleteKandangClickHandler = () => {
|
const deleteKandangClickHandler = () => {
|
||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
};
|
};
|
||||||
@@ -269,6 +295,24 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
required
|
||||||
|
label='Kandang Group'
|
||||||
|
value={formik.values.group ?? undefined}
|
||||||
|
onChange={kandangGroupChangeHandler}
|
||||||
|
options={kandangGroupOptions}
|
||||||
|
onInputChange={setKandangGroupSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandangGroups}
|
||||||
|
isLoading={isLoadingKandangGroupOptions}
|
||||||
|
isError={formik.touched.group && Boolean(formik.errors.group)}
|
||||||
|
errorMessage={
|
||||||
|
getIn(formik.errors.group, 'value') ??
|
||||||
|
(formik.errors.group as string)
|
||||||
|
}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
'lti.daily_checklist.master_data.employee',
|
'lti.daily_checklist.master_data.employee',
|
||||||
'lti.daily_checklist.master_data.activity',
|
'lti.daily_checklist.master_data.activity',
|
||||||
'lti.daily_checklist.master_data.configuration',
|
'lti.daily_checklist.master_data.configuration',
|
||||||
|
'lti.daily_checklist.master_data.kandang',
|
||||||
],
|
],
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
@@ -66,6 +67,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
link: '/daily-checklist/master-data/activity',
|
link: '/daily-checklist/master-data/activity',
|
||||||
permission: ['lti.daily_checklist.master_data.activity'],
|
permission: ['lti.daily_checklist.master_data.activity'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Kandang',
|
||||||
|
link: '/daily-checklist/master-data/kandang',
|
||||||
|
permission: ['lti.daily_checklist.master_data.kandang'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Konfigurasi',
|
text: 'Konfigurasi',
|
||||||
link: '/daily-checklist/master-data/configuration',
|
link: '/daily-checklist/master-data/configuration',
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/daily-checklist/master-data/configuration/': [
|
'/daily-checklist/master-data/configuration/': [
|
||||||
'lti.daily_checklist.master_data.configuration',
|
'lti.daily_checklist.master_data.configuration',
|
||||||
],
|
],
|
||||||
|
'/daily-checklist/master-data/kandang/': [
|
||||||
|
'lti.daily_checklist.master_data.kandang',
|
||||||
|
],
|
||||||
|
|
||||||
// Production
|
// Production
|
||||||
// Production - Project Flock
|
// Production - Project Flock
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Check, ChevronsUpDown, X } from 'lucide-react';
|
import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +29,8 @@ interface MultiSelectProps {
|
|||||||
selected: string[];
|
selected: string[];
|
||||||
onChange: (selected: string[]) => void;
|
onChange: (selected: string[]) => void;
|
||||||
onSearchChange?: (value: string) => void;
|
onSearchChange?: (value: string) => void;
|
||||||
|
onLoadMore?: () => void;
|
||||||
|
isLoadingMore?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -39,6 +41,8 @@ export function MultiSelect({
|
|||||||
selected,
|
selected,
|
||||||
onChange,
|
onChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
|
onLoadMore,
|
||||||
|
isLoadingMore,
|
||||||
placeholder = 'Select items...',
|
placeholder = 'Select items...',
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
@@ -115,7 +119,18 @@ export function MultiSelect({
|
|||||||
onValueChange={onSearchChange}
|
onValueChange={onSearchChange}
|
||||||
/>
|
/>
|
||||||
<CommandEmpty>No item found.</CommandEmpty>
|
<CommandEmpty>No item found.</CommandEmpty>
|
||||||
<CommandList className='max-h-[300px] overflow-y-auto'>
|
<CommandList
|
||||||
|
className='max-h-[300px] overflow-y-auto'
|
||||||
|
onScroll={(e) => {
|
||||||
|
const target = e.currentTarget;
|
||||||
|
if (
|
||||||
|
target.scrollHeight - target.scrollTop <=
|
||||||
|
target.clientHeight + 1
|
||||||
|
) {
|
||||||
|
onLoadMore?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CommandGroup className='overflow-visible'>
|
<CommandGroup className='overflow-visible'>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
@@ -134,6 +149,11 @@ export function MultiSelect({
|
|||||||
{option.label}
|
{option.label}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className='py-4 flex justify-center w-full'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-muted-foreground' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ function SelectContent({
|
|||||||
children,
|
children,
|
||||||
position = 'popper',
|
position = 'popper',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
|
||||||
|
onScroll?: React.UIEventHandler<HTMLDivElement>;
|
||||||
|
}) {
|
||||||
|
const { onScroll, ...restProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
@@ -67,7 +71,7 @@ function SelectContent({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
{...props}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
<SelectScrollUpButton />
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
@@ -76,6 +80,7 @@ function SelectContent({
|
|||||||
position === 'popper' &&
|
position === 'popper' &&
|
||||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
|
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
|
||||||
)}
|
)}
|
||||||
|
onScroll={onScroll}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</SelectPrimitive.Viewport>
|
</SelectPrimitive.Viewport>
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, X, Save, Send, Info, FilePlus, ListChecks } from 'lucide-react';
|
import {
|
||||||
|
Plus,
|
||||||
|
X,
|
||||||
|
Save,
|
||||||
|
Send,
|
||||||
|
Info,
|
||||||
|
FilePlus,
|
||||||
|
ListChecks,
|
||||||
|
Loader2,
|
||||||
|
} from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import { Label } from '@/figma-make/components/base/label';
|
import { Label } from '@/figma-make/components/base/label';
|
||||||
@@ -26,7 +35,6 @@ import {
|
|||||||
import { DatePicker } from '@/figma-make/components/base/date-picker';
|
import { DatePicker } from '@/figma-make/components/base/date-picker';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
|
||||||
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -43,6 +51,7 @@ import DropFileInput from '@/components/input/DropFileInput';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
// Static categories
|
// Static categories
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
@@ -86,16 +95,11 @@ export function DailyChecklistContent() {
|
|||||||
searchParams.get('category') || ''
|
searchParams.get('category') || ''
|
||||||
);
|
);
|
||||||
|
|
||||||
const { options: kandangOptions } = useSelect(
|
const {
|
||||||
KandangApi.basePath,
|
options: kandangOptions,
|
||||||
'id',
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
'name',
|
loadMore: loadMoreKandang,
|
||||||
'search',
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
{
|
|
||||||
page: '1',
|
|
||||||
limit: '100',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: phases } = useSWR<
|
const { data: phases } = useSWR<
|
||||||
BaseApiResponse<Phase[] | undefined>,
|
BaseApiResponse<Phase[] | undefined>,
|
||||||
@@ -168,6 +172,16 @@ export function DailyChecklistContent() {
|
|||||||
const [documents, setDocuments] = useState<File[]>([]);
|
const [documents, setDocuments] = useState<File[]>([]);
|
||||||
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
|
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
|
||||||
|
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||||
|
if (!isLoadingMoreKandang) {
|
||||||
|
loadMoreKandang();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Sync state to URL query params
|
// Sync state to URL query params
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
@@ -994,7 +1008,7 @@ export function DailyChecklistContent() {
|
|||||||
>
|
>
|
||||||
<SelectValue placeholder='Pilih kandang' />
|
<SelectValue placeholder='Pilih kandang' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent onScroll={handleKandangScroll}>
|
||||||
{kandangOptions.map((kandang) => (
|
{kandangOptions.map((kandang) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={kandang.value}
|
key={kandang.value}
|
||||||
@@ -1003,6 +1017,12 @@ export function DailyChecklistContent() {
|
|||||||
{kandang.label}
|
{kandang.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{isLoadingMoreKandang && (
|
||||||
|
<div className='flex justify-center p-2'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+30
-13
@@ -1,7 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
|
import {
|
||||||
|
Eye,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
Search,
|
||||||
|
Trash2,
|
||||||
|
Edit,
|
||||||
|
Loader2,
|
||||||
|
} from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import { Badge } from '@/figma-make/components/base/badge';
|
import { Badge } from '@/figma-make/components/base/badge';
|
||||||
@@ -34,9 +42,9 @@ import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: 'ALL', label: 'Semua Status' },
|
{ value: 'ALL', label: 'Semua Status' },
|
||||||
@@ -93,21 +101,25 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { options: kandangOptions } = useSelect(
|
const {
|
||||||
KandangApi.basePath,
|
options: kandangOptions,
|
||||||
'id',
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
'name',
|
loadMore: loadMoreKandang,
|
||||||
'search',
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
{
|
|
||||||
page: '1',
|
|
||||||
limit: '100',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const checklistList = isResponseSuccess(checklistListRes)
|
const checklistList = isResponseSuccess(checklistListRes)
|
||||||
? checklistListRes.data || []
|
? checklistListRes.data || []
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||||
|
if (!isLoadingMoreKandang) {
|
||||||
|
loadMoreKandang();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
@@ -490,7 +502,7 @@ export function ListDailyChecklistContent() {
|
|||||||
>
|
>
|
||||||
<SelectValue placeholder='Semua Kandang' />
|
<SelectValue placeholder='Semua Kandang' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent onScroll={handleKandangScroll}>
|
||||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||||
{kandangOptions.map((kandang) => (
|
{kandangOptions.map((kandang) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -500,6 +512,11 @@ export function ListDailyChecklistContent() {
|
|||||||
{kandang.label}
|
{kandang.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{isLoadingMoreKandang && (
|
||||||
|
<div className='flex justify-center p-2'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
|
import { Label } from '@/figma-make/components/base/label';
|
||||||
|
import { Input } from '@/figma-make/components/base/input';
|
||||||
|
import { Badge } from '@/figma-make/components/base/badge';
|
||||||
|
import { MultiSelect } from '@/figma-make/components/base/multi-select';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/figma-make/components/base/select';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/figma-make/components/base/dialog';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/figma-make/components/base/alert-dialog';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/figma-make/components/base/dropdown-menu';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
|
import { UserApi } from '@/services/api/user';
|
||||||
|
|
||||||
|
export function MasterKandangContent() {
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
location_id: '',
|
||||||
|
status: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
search: 'search',
|
||||||
|
location_id: 'location_id',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: dailyChecklistKandangs,
|
||||||
|
isLoading: isLoadingDailyChecklistKandangs,
|
||||||
|
mutate: refreshDailyChecklistKandangs,
|
||||||
|
} = useSWR(
|
||||||
|
`${DailyChecklistKandangApi.basePath}${getTableFilterQueryString()}`,
|
||||||
|
DailyChecklistKandangApi.getAllFetcher,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { options: locationOptions } = useSelect(
|
||||||
|
LocationApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
page: '1',
|
||||||
|
limit: '100',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { options: picOptions } = useSelect(
|
||||||
|
UserApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
page: '1',
|
||||||
|
limit: '100',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
options: kandangOptions,
|
||||||
|
isLoadingMore: isLoadingKandangOptionsMore,
|
||||||
|
loadMore: loadMoreKandang,
|
||||||
|
} = useSelect(KandangApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
|
||||||
|
const [kandangForm, setKandangForm] = useState({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
location_id: 0,
|
||||||
|
pic_id: 0,
|
||||||
|
// recording_kandangs: [] as number[],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dailyChecklistKandangColumns: ColumnDef<DailyChecklistKandang>[] = [
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
header: 'Nama',
|
||||||
|
accessorKey: 'name',
|
||||||
|
enableSorting: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'location',
|
||||||
|
header: 'Lokasi',
|
||||||
|
accessorKey: 'location',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => row.original.location.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pic',
|
||||||
|
header: 'PIC',
|
||||||
|
accessorKey: 'pic',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => row.original.pic.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'recording_kandangs',
|
||||||
|
header: 'Kandang Recording',
|
||||||
|
accessorKey: 'recording_kandangs',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.recording_kandangs?.length > 0
|
||||||
|
? row.original.recording_kandangs.map((item) => item.name).join(', ')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'action',
|
||||||
|
header: 'Aksi',
|
||||||
|
accessorKey: 'action',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
size='sm'
|
||||||
|
className='h-8 w-8 p-0 hover:bg-gray-100'
|
||||||
|
>
|
||||||
|
<MoreVertical className='h-4 w-4 text-gray-600' />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
|
||||||
|
<Pencil className='mr-2 h-4 w-4' />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDeleteClick(row.original.id)}
|
||||||
|
className='text-red-600'
|
||||||
|
>
|
||||||
|
<Trash2 className='mr-2 h-4 w-4' />
|
||||||
|
Hapus
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setModalMode('create');
|
||||||
|
setKandangForm({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
location_id: 0,
|
||||||
|
pic_id: 0,
|
||||||
|
// recording_kandangs: []
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (dailyChecklistKandang: DailyChecklistKandang) => {
|
||||||
|
setModalMode('edit');
|
||||||
|
setKandangForm({
|
||||||
|
id: dailyChecklistKandang.id,
|
||||||
|
name: dailyChecklistKandang.name,
|
||||||
|
location_id: dailyChecklistKandang.location.id,
|
||||||
|
pic_id: dailyChecklistKandang.pic.id,
|
||||||
|
// recording_kandangs:
|
||||||
|
// dailyChecklistKandang.recording_kandangs.map((item) => item.id) ?? [],
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!kandangForm.name.trim()) {
|
||||||
|
toast.error('Nama harus diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kandangForm.location_id) {
|
||||||
|
toast.error('Lokasi wajib diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (!kandangForm.recording_kandangs.length) {
|
||||||
|
// toast.error('Kandang recording wajib diisi');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (modalMode === 'create') {
|
||||||
|
const createDailyChecklistKandangResponse =
|
||||||
|
await DailyChecklistKandangApi.create({
|
||||||
|
name: kandangForm.name.trim(),
|
||||||
|
location_id: kandangForm.location_id,
|
||||||
|
pic_id: kandangForm.pic_id,
|
||||||
|
// recording_kandang_ids: kandangForm.recording_kandangs,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseError(createDailyChecklistKandangResponse)) {
|
||||||
|
console.error(
|
||||||
|
'Error creating kandang:',
|
||||||
|
createDailyChecklistKandangResponse.message
|
||||||
|
);
|
||||||
|
toast.error('Gagal menambahkan kandang');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshDailyChecklistKandangs();
|
||||||
|
toast.success('Kandang berhasil ditambahkan');
|
||||||
|
} else {
|
||||||
|
const updateDailyChecklistKandangResponse =
|
||||||
|
await DailyChecklistKandangApi.update(kandangForm.id, {
|
||||||
|
name: kandangForm.name.trim(),
|
||||||
|
location_id: kandangForm.location_id,
|
||||||
|
pic_id: kandangForm.pic_id,
|
||||||
|
// recording_kandang_ids: kandangForm.recording_kandangs,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseError(updateDailyChecklistKandangResponse)) {
|
||||||
|
console.error(
|
||||||
|
'Error updating kandang:',
|
||||||
|
updateDailyChecklistKandangResponse.message
|
||||||
|
);
|
||||||
|
toast.error('Gagal menambahkan Kandang');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshDailyChecklistKandangs();
|
||||||
|
toast.success('Kandang berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowModal(false);
|
||||||
|
setKandangForm({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
location_id: 0,
|
||||||
|
pic_id: 0,
|
||||||
|
// recording_kandangs: [],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving kandang:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat menyimpan kandang');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (kandangId: number) => {
|
||||||
|
setKandangToDelete(kandangId);
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = async () => {
|
||||||
|
if (!kandangToDelete) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleteKandangResponse =
|
||||||
|
await DailyChecklistKandangApi.delete(kandangToDelete);
|
||||||
|
|
||||||
|
if (isResponseError(deleteKandangResponse)) {
|
||||||
|
console.error('Error deleting kandang:', deleteKandangResponse.message);
|
||||||
|
toast.error('Gagal menghapus kandang');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshDailyChecklistKandangs();
|
||||||
|
toast.success('Kandang berhasil dihapus');
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setKandangToDelete(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting kandang:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat menghapus kandang');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoadingDailyChecklistKandangs && !dailyChecklistKandangs) {
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen'>
|
||||||
|
<div className='p-6'>
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
|
Master Kandang
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
|
Master Data • <span className='text-[#0069e0]'>Kandang</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
|
<CardContent className='p-12 text-center text-gray-500'>
|
||||||
|
Memuat data...
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen'>
|
||||||
|
<div className='p-6'>
|
||||||
|
{/* Page Title */}
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
|
Master Kandang
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
|
Master Data • <span className='text-[#0069e0]'>Kandang</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Card */}
|
||||||
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
|
<CardContent className='p-0'>
|
||||||
|
{/* Single Toolbar Row */}
|
||||||
|
<div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
|
||||||
|
{/* LEFT: Search + Filters */}
|
||||||
|
<div className='flex items-center gap-3 flex-wrap'>
|
||||||
|
<div className='relative'>
|
||||||
|
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
|
||||||
|
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari kandang...'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={(e) => updateFilter('search', e.target.value)}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full sm:w-[280px] border-gray-200',
|
||||||
|
inputWrapper: 'px-3 py-2 h-fit rounded-md',
|
||||||
|
input: 'text-sm',
|
||||||
|
}}
|
||||||
|
startAdornment={
|
||||||
|
<Search className='text-gray-400 w-4 h-4' />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={tableFilterState.location_id}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateFilter('location_id', value === 'all' ? '' : value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className='w-[180px] border-gray-200'>
|
||||||
|
<SelectValue placeholder='Semua Lokasi' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='all'>Semua Lokasi</SelectItem>
|
||||||
|
{locationOptions.map((kandang) => (
|
||||||
|
<SelectItem
|
||||||
|
key={kandang.value}
|
||||||
|
value={String(kandang.value)}
|
||||||
|
>
|
||||||
|
{kandang.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RIGHT: Export + Add */}
|
||||||
|
<div className='flex items-center gap-2 flex-wrap'>
|
||||||
|
<Button
|
||||||
|
onClick={handleAdd}
|
||||||
|
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
||||||
|
>
|
||||||
|
<Plus className='w-4 h-4 mr-2' />
|
||||||
|
Tambah Kandang
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<Table<DailyChecklistKandang>
|
||||||
|
data={
|
||||||
|
isResponseSuccess(dailyChecklistKandangs)
|
||||||
|
? dailyChecklistKandangs?.data
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
columns={dailyChecklistKandangColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(dailyChecklistKandangs)
|
||||||
|
? dailyChecklistKandangs?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(dailyChecklistKandangs)
|
||||||
|
? dailyChecklistKandangs?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoadingDailyChecklistKandangs}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'w-full mb-20':
|
||||||
|
isResponseSuccess(dailyChecklistKandangs) &&
|
||||||
|
dailyChecklistKandangs?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
tableWrapperClassName:
|
||||||
|
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
|
||||||
|
headerRowClassName: 'bg-gray-50/50',
|
||||||
|
headerColumnClassName:
|
||||||
|
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
|
||||||
|
paginationClassName: 'px-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add/Edit Modal */}
|
||||||
|
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||||
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{modalMode === 'create' ? 'Tambah Kandang' : 'Edit Kandang'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{modalMode === 'create'
|
||||||
|
? 'Masukkan detail Kandang baru'
|
||||||
|
: 'Ubah detail Kandang'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className='space-y-4 py-4'>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='nama-kandang'>
|
||||||
|
Nama Kandang <span className='text-red-500'>*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id='nama-kandang'
|
||||||
|
value={kandangForm.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setKandangForm({ ...kandangForm, name: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder='Masukkan nama Kandang'
|
||||||
|
className='mt-1.5'
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='category'>
|
||||||
|
Lokasi <span className='text-red-500'>*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
kandangForm.location_id ? String(kandangForm.location_id) : ''
|
||||||
|
}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setKandangForm({ ...kandangForm, location_id: Number(value) })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id='category' className='mt-1.5'>
|
||||||
|
<SelectValue placeholder='Pilih lokasi' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{locationOptions.map((cat) => (
|
||||||
|
<SelectItem key={cat.value} value={String(cat.value)}>
|
||||||
|
{cat.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='pic'>
|
||||||
|
PIC <span className='text-red-500'>*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={kandangForm.pic_id ? String(kandangForm.pic_id) : ''}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setKandangForm({ ...kandangForm, pic_id: Number(value) })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id='pic' className='mt-1.5'>
|
||||||
|
<SelectValue placeholder='Pilih PIC' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{picOptions.map((cat) => (
|
||||||
|
<SelectItem key={cat.value} value={String(cat.value)}>
|
||||||
|
{cat.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
||||||
|
>
|
||||||
|
{loading ? 'Menyimpan...' : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation */}
|
||||||
|
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||||
|
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Hapus Kandang?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Data Kandang akan dihapus secara permanen.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
disabled={loading}
|
||||||
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
|
>
|
||||||
|
{loading ? 'Menghapus...' : 'Hapus'}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { BaseApiService } from '@/services/api/base';
|
||||||
|
import {
|
||||||
|
DailyChecklistKandang,
|
||||||
|
CreateDailyChecklistKandangPayload,
|
||||||
|
UpdateDailyChecklistKandangPayload,
|
||||||
|
} from '@/types/api/daily-checklist/kandang';
|
||||||
|
|
||||||
|
export class DailyChecklistKandangApiService extends BaseApiService<
|
||||||
|
DailyChecklistKandang,
|
||||||
|
CreateDailyChecklistKandangPayload,
|
||||||
|
UpdateDailyChecklistKandangPayload
|
||||||
|
> {
|
||||||
|
constructor(basePath: string = '/master-data/kandang-groups') {
|
||||||
|
super(basePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DailyChecklistKandangApi = new DailyChecklistKandangApiService(
|
||||||
|
'/master-data/kandang-groups'
|
||||||
|
);
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
|
import { BaseKandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { BaseLocation } from '@/types/api/master-data/location';
|
||||||
|
import { BaseUser } from '@/types/api/user';
|
||||||
|
|
||||||
|
export type BaseDailyChecklistKandang = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
location: BaseLocation;
|
||||||
|
recording_kandangs: Pick<BaseKandang, 'id' | 'name'>[];
|
||||||
|
pic: BaseUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DailyChecklistKandang = BaseMetadata & BaseDailyChecklistKandang;
|
||||||
|
|
||||||
|
export type CreateDailyChecklistKandangPayload = {
|
||||||
|
name: string;
|
||||||
|
location_id: number;
|
||||||
|
pic_id: number;
|
||||||
|
// recording_kandang_ids: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateDailyChecklistKandangPayload =
|
||||||
|
CreateDailyChecklistKandangPayload;
|
||||||
+3
@@ -1,6 +1,7 @@
|
|||||||
import { BaseMetadata } from '@/types/api/api-general';
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
import { BaseLocation } from '@/types/api/master-data/location';
|
import { BaseLocation } from '@/types/api/master-data/location';
|
||||||
import { BaseUser } from '@/types/api/user';
|
import { BaseUser } from '@/types/api/user';
|
||||||
|
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
|
|
||||||
export type BaseKandang = {
|
export type BaseKandang = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -10,6 +11,7 @@ export type BaseKandang = {
|
|||||||
capacity: number;
|
capacity: number;
|
||||||
pic: BaseUser;
|
pic: BaseUser;
|
||||||
project_flock_kandang_id?: number;
|
project_flock_kandang_id?: number;
|
||||||
|
kandang_group: Pick<BaseDailyChecklistKandang, 'id' | 'name'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Kandang = BaseMetadata & BaseKandang;
|
export type Kandang = BaseMetadata & BaseKandang;
|
||||||
@@ -19,6 +21,7 @@ export type CreateKandangPayload = {
|
|||||||
location_id: number;
|
location_id: number;
|
||||||
capacity: number;
|
capacity: number;
|
||||||
pic_id: number;
|
pic_id: number;
|
||||||
|
group_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateKandangPayload = CreateKandangPayload;
|
export type UpdateKandangPayload = CreateKandangPayload;
|
||||||
|
|||||||
Reference in New Issue
Block a user