diff --git a/.husky/pre-commit b/.husky/pre-commit index de6b606b..0836a1c5 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ -npm run format -npm run lint -npm run typecheck -git add . +#npm run format +#npm run lint +#npm run typecheck +#git add . diff --git a/src/components/pages/expense/ExpenseDetail.tsx b/src/components/pages/expense/ExpenseDetail.tsx index c09b168a..f5da407f 100644 --- a/src/components/pages/expense/ExpenseDetail.tsx +++ b/src/components/pages/expense/ExpenseDetail.tsx @@ -1,7 +1,7 @@ 'use client'; import { useMemo, useState } from 'react'; -import { useSearchParams } from 'next/navigation'; +import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -10,16 +10,14 @@ import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestCont import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent'; import { Expense } from '@/types/api/expense'; -import { getExpenseListReturnTo } from '@/lib/expense-list-navigation'; interface ExpenseDetailProps { initialValues?: Expense; } const ExpenseDetail: React.FC = ({ initialValues }) => { + const router = useRouter(); const [activeTab, setActiveTab] = useState('request'); - const searchParams = useSearchParams(); - const returnTo = getExpenseListReturnTo(searchParams); const expenseDetailTabs = useMemo(() => { const validTabs = [ @@ -50,8 +48,8 @@ const ExpenseDetail: React.FC = ({ initialValues }) => {
@@ -765,7 +765,7 @@ const MarketingTable = () => { width={20} height={20} /> - Approve + Approve ({idsToProcess.length} Item) diff --git a/src/components/pages/purchase/PurchaseFilterModal.tsx b/src/components/pages/purchase/PurchaseFilterModal.tsx index bcb80fe6..856f2074 100644 --- a/src/components/pages/purchase/PurchaseFilterModal.tsx +++ b/src/components/pages/purchase/PurchaseFilterModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject, useState, useEffect, useMemo } from 'react'; +import { RefObject, useState, useEffect, useMemo, useCallback } from 'react'; import { useFormik } from 'formik'; import toast from 'react-hot-toast'; @@ -26,22 +26,32 @@ import { isResponseSuccess } from '@/lib/api-helper'; interface PurchaseFilterModalProps { ref: RefObject; + initialValues?: { + poDate: string; + category: OptionType[]; + status: OptionType[]; + supplier: OptionType | null; + area: OptionType | null; + location: OptionType | null; + project_flock: OptionType | null; + project_flock_kandang: OptionType | null; + }; onSubmit?: (values: PurchaseFilter) => void; onReset?: () => void; } const PurchaseFilterModal = ({ ref, + initialValues, onSubmit, onReset, }: PurchaseFilterModalProps) => { - const closeModalHandler = () => { + const closeModalHandler = useCallback(() => { ref.current?.close(); - }; + }, [ref]); // ===== DATE ERROR STATE ===== const [dateErrorShown, setDateErrorShown] = useState(false); - const [hasDateError, setHasDateError] = useState(false); // ===== CLEANUP TOAST ON UNMOUNT ===== useEffect(() => { @@ -81,8 +91,12 @@ const PurchaseFilterModal = ({ 'search' ); - const [selectedAreaId, setSelectedAreaId] = useState(''); - const [selectedLocationId, setSelectedLocationId] = useState(''); + const [selectedAreaId, setSelectedAreaId] = useState( + initialValues?.area?.value ? String(initialValues.area.value) : '' + ); + const [selectedLocationId, setSelectedLocationId] = useState( + initialValues?.location?.value ? String(initialValues.location.value) : '' + ); const { setInputValue: setSupplierInputValue, @@ -133,7 +147,8 @@ const PurchaseFilterModal = ({ project_flock: OptionType | null; project_flock_kandang: OptionType | null; }>({ - initialValues: { + // enableReinitialize: true, + initialValues: initialValues || { poDate: '', category: [], status: [], @@ -147,12 +162,18 @@ const PurchaseFilterModal = ({ const formattedValues = { ...values, category: values.category.map((item) => String(item.value)), + category_labels: values.category, status: values.status.map((item) => String(item.value)), supplier_id: values.supplier?.value, + supplier_label: values.supplier?.label, area_id: values.area?.value, + area_label: values.area?.label, location_id: values.location?.value, + location_label: values.location?.label, project_flock_id: values.project_flock?.value, + project_flock_label: values.project_flock?.label, project_flock_kandang_id: values.project_flock_kandang?.value, + project_flock_kandang_label: values.project_flock_kandang?.label, }; onSubmit?.(formattedValues); @@ -166,6 +187,17 @@ const PurchaseFilterModal = ({ }, }); + const { resetForm, submitForm } = formik; + + useEffect(() => { + setSelectedAreaId( + initialValues?.area?.value ? String(initialValues.area.value) : '' + ); + setSelectedLocationId( + initialValues?.location?.value ? String(initialValues.location.value) : '' + ); + }, [initialValues?.area, initialValues?.location]); + const projectFlockKandangOptions = useMemo(() => { if ( !formik.values.project_flock || @@ -197,6 +229,29 @@ const PurchaseFilterModal = ({ formik.setFieldValue('status', val); }; + const formikResetHandler = useCallback(() => { + resetForm({ + values: { + poDate: '', + category: [], + status: [], + supplier: null, + area: null, + location: null, + project_flock: null, + project_flock_kandang: null, + }, + }); + setSelectedAreaId(''); + setSelectedLocationId(''); + onReset?.(); + closeModalHandler(); + }, [resetForm, onReset, closeModalHandler]); + + const formikSubmitHandler = useCallback(async () => { + await submitForm(); + }, [submitForm]); + return (
{/* Modal Header */} @@ -220,7 +275,9 @@ const PurchaseFilterModal = ({ type='button' variant='ghost' color='none' - onClick={closeModalHandler} + onClick={() => { + closeModalHandler(); + }} className='p-0 text-base-content/50 hover:text-base-content' > @@ -377,7 +434,8 @@ const PurchaseFilterModal = ({ - - )} - - {selectedEmployees.length > 0 ? ( -
- {selectedEmployees.map((emp) => ( - 0 && ( +
+ {isChecklistStatusDraft && ( +
+ + - )} - - ))} -
- ) : ( -

Belum ada ABK dipilih

- )} -
- )} + + Tambah ABK + +
+ )} + + {selectedEmployees.length > 0 ? ( +
+ {selectedEmployees.map((emp) => ( + + {emp.name} + {isChecklistStatusDraft && ( + + )} + + ))} +
+ ) : ( +

+ Belum ada ABK dipilih +

+ )} + + )} {/* Activity Checklist Table */} - {dailyChecklistId && - selectedPhaseIds.length > 0 && - selectedEmployees.length > 0 ? ( -
-

- Checklist Aktivitas -

- {Object.keys(activitiesByPhase).length > 0 ? ( -
- - - - - {sortedSelectedEmployees.map((emp) => ( - - ))} - - - - - {Object.keys(groupActivitiesByPhase()).flatMap( - (phaseId) => { - const phaseData = groupActivitiesByPhase()[phaseId]; - const { phase, timeGroups } = phaseData; - - const timeTypes = Object.keys(timeGroups).sort( - (a, b) => - TIME_TYPE_ORDER.indexOf(a) - - TIME_TYPE_ORDER.indexOf(b) - ); - - // Count total activities in this phase - const totalActivities = timeTypes.reduce( - (sum, timeType) => - sum + timeGroups[timeType].length, - 0 - ); - - // Build all rows for this phase - const rows = []; - - // PHASE Header (Main parent) - BLUE - rows.push( - - + + {sortedSelectedEmployees.map((emp) => ( + + ))} + + + ); + }); + }); + + return rows; + } + )} + +
- Aktivitas - - {emp.name} - - Catatan -
+ {dailyChecklistId && + selectedPhaseIds.length > 0 && + selectedEmployees.length > 0 ? ( +
+

+ Checklist Aktivitas +

+ {Object.keys(activitiesByPhase).length > 0 ? ( +
+ + + + + {sortedSelectedEmployees.map((emp) => ( + - ); + {emp.name} + + ))} + + + + + {Object.keys(groupActivitiesByPhase()).flatMap( + (phaseId) => { + const phaseData = + groupActivitiesByPhase()[phaseId]; + const { phase, timeGroups } = phaseData; - // TIME_TYPE sub-headers and activities - timeTypes.forEach((timeType) => { - const activities = timeGroups[timeType]; - const hasMultipleTimeTypes = timeTypes.length > 1; + const timeTypes = Object.keys(timeGroups).sort( + (a, b) => + TIME_TYPE_ORDER.indexOf(a) - + TIME_TYPE_ORDER.indexOf(b) + ); - // TIME Header (optional, only if phase has multiple time types) - GRAY SOFT - if (hasMultipleTimeTypes) { + // Count total activities in this phase + const totalActivities = timeTypes.reduce( + (sum, timeType) => + sum + timeGroups[timeType].length, + 0 + ); + + // Build all rows for this phase + const rows = []; + + // PHASE Header (Main parent) - BLUE rows.push( ); - } - // ACTIVITY rows (Child rows with checkboxes) - activities.sort((a, b) => - a.name.localeCompare(b.name, undefined, { - sensitivity: 'base', - }) - ); + // TIME_TYPE sub-headers and activities + timeTypes.forEach((timeType) => { + const activities = timeGroups[timeType]; + const hasMultipleTimeTypes = + timeTypes.length > 1; - activities.forEach((activity, index) => { - const taskId = - taskIdsByPhaseActivityId[activity.id]; - const indentClass = hasMultipleTimeTypes - ? 'pl-12' - : 'pl-8'; - - rows.push( - - - {sortedSelectedEmployees.map((emp) => ( - - ))} - - - ); - }); - }); + + + ); + } - return rows; - } - )} - -
+ Aktivitas + -
- - {phase.name} - - - {totalActivities} aktivitas - -
- -
+ Catatan +
- - {TIME_TYPE_LABELS[timeType]} ( - {activities.length} aktivitas) - +
+ + {phase.name} + + + {totalActivities} aktivitas + +
-

- {activity.name} -

- {activity.description && ( -

- {activity.description} -

- )} -
- - handleCheckboxChange( - String(activity.id), - String(emp.id), - e.target.checked - ) - } - disabled={!isChecklistStatusDraft} - className='checkbox-clean' - /> - - 0 - ? assignments[taskId]?.[ - selectedEmployees[0].id - ]?.note || '' - : '' - } - onChange={(e) => { - if (selectedEmployees.length > 0) { - handleNoteChange( - String(activity.id), - String(selectedEmployees[0].id), - e.target.value - ); - } - }} - disabled={!isChecklistStatusDraft} - /> -
+ + {TIME_TYPE_LABELS[timeType]} ( + {activities.length} aktivitas) + +
+ // ACTIVITY rows (Child rows with checkboxes) + activities.sort((a, b) => + a.name.localeCompare(b.name, undefined, { + sensitivity: 'base', + }) + ); + + activities.forEach((activity, index) => { + const taskId = + taskIdsByPhaseActivityId[activity.id]; + const indentClass = hasMultipleTimeTypes + ? 'pl-12' + : 'pl-8'; + + rows.push( +
+

+ {activity.name} +

+ {activity.description && ( +

+ {activity.description} +

+ )} +
+ + handleCheckboxChange( + String(activity.id), + String(emp.id), + e.target.checked + ) + } + disabled={!isChecklistStatusDraft} + className='checkbox-clean' + /> + + 0 + ? assignments[taskId]?.[ + selectedEmployees[0].id + ]?.note || '' + : '' + } + onChange={(e) => { + if ( + selectedEmployees.length > 0 + ) { + handleNoteChange( + String(activity.id), + String( + selectedEmployees[0].id + ), + e.target.value + ); + } + }} + disabled={!isChecklistStatusDraft} + /> +
+
+ ) : ( +
+ +

+ Tidak Ada Aktivitas +

+

+ Tidak ada aktivitas untuk fase yang dipilih. Silakan + tambahkan aktivitas di Master Aktivitas. +

+
+ )}
) : (
- -

- Tidak Ada Aktivitas -

-

- Tidak ada aktivitas untuk fase yang dipilih. Silakan - tambahkan aktivitas di Master Aktivitas. -

+ {!dailyChecklistId ? ( +
+ +

+ Mulai Checklist Baru +

+

+ Pilih tanggal, kandang, dan kategori untuk memulai + checklist harian Anda. +

+
+ ) : selectedPhaseIds.length === 0 ? ( +
+ +

+ Pilih Fase / Tahap +

+

+ Klik tombol {'"'}Pilih Fase{'"'} untuk memilih tahap + aktivitas yang akan dikerjakan. +

+
+ ) : ( +
+ +

+ Pilih ABK +

+

+ Klik tombol {'"'}Tambah ABK{'"'} untuk memilih pekerja + yang akan ditugaskan. +

+
+ )}
)} - - ) : ( -
- {!dailyChecklistId ? ( -
- -

- Mulai Checklist Baru -

-

- Pilih tanggal, kandang, dan kategori untuk memulai - checklist harian Anda. -

-
- ) : selectedPhaseIds.length === 0 ? ( -
- -

- Pilih Fase / Tahap -

-

- Klik tombol {'"'}Pilih Fase{'"'} untuk memilih tahap - aktivitas yang akan dikerjakan. -

-
- ) : ( -
- -

- Pilih ABK -

-

- Klik tombol {'"'}Tambah ABK{'"'} untuk memilih pekerja - yang akan ditugaskan. -

-
- )} -
+ )} - {dailyChecklistId && + {!isKandangEmpty && + dailyChecklistId && selectedPhaseIds.length > 0 && selectedEmployees.length > 0 && ( <> @@ -1548,7 +1572,8 @@ export function DailyChecklistContent() { )} {/* Action Buttons */} - {dailyChecklistId && + {!isKandangEmpty && + dailyChecklistId && selectedPhaseIds.length > 0 && selectedEmployees.length > 0 && isChecklistStatusDraft && ( diff --git a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx index c9d8d21d..d927d312 100644 --- a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx @@ -2,7 +2,14 @@ import { useState, useEffect } from 'react'; import * as React from 'react'; -import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; +import { + ArrowLeft, + CheckCircle, + XCircle, + AlertCircle, + Share2, +} from 'lucide-react'; +import * as htmlToImage from 'html-to-image'; import { Card, CardContent } from '@/figma-make/components/base/card'; import { Button } from '@/figma-make/components/base/button'; import { Badge } from '@/figma-make/components/base/badge'; @@ -137,6 +144,8 @@ export function DetailDailyChecklistContent() { const [rejectReason, setRejectReason] = useState(''); const [actionLoading, setActionLoading] = useState(false); + const [isGeneratingImage, setIsGeneratingImage] = useState(false); + useEffect(() => { if (checklistId) { fetchChecklistDetail(); @@ -547,6 +556,42 @@ export function DetailDailyChecklistContent() { }); }; + const shareHandler = async () => { + setIsGeneratingImage(true); + + const htmlBlob = await htmlToImage.toBlob(document.body); + const imgFile = new File( + [htmlBlob!], + `daily-checklist-${header?.date}-${header?.kandang_name}-${header?.category}.png`, + { + type: 'image/png', + } + ); + + setIsGeneratingImage(false); + + const shareData = { + files: [imgFile], + title: `Daily Checklist - ${formatDate(header?.date || '')} - ${header?.kandang_name} - ${header?.category}`, + text: `Daily Checklist - ${formatDate(header?.date || '')} - ${header?.kandang_name} - ${header?.category}`, + url: window.location.href, + }; + + try { + if (!navigator.canShare(shareData)) { + toast.error( + 'Gagal membagikan checklist, coba dengan perangkat yang berbeda' + ); + return; + } + + await navigator.share(shareData); + toast.success('Checklist berhasil dibagikan'); + } catch (error) { + toast.error('Gagal membagikan checklist'); + } + }; + if (loading) { return (
@@ -584,6 +629,7 @@ export function DetailDailyChecklistContent() { Kembali +

Detail Daily Checklist @@ -592,6 +638,7 @@ export function DetailDailyChecklistContent() { Lihat detail checklist harian

+ {header.status === 'SUBMITTED' && (
@@ -615,6 +662,19 @@ export function DetailDailyChecklistContent() {
)} + +
{/* Header Info Card */} diff --git a/src/services/hooks/useTableFilter.tsx b/src/services/hooks/useTableFilter.tsx index 43fc173c..5db57bcb 100644 --- a/src/services/hooks/useTableFilter.tsx +++ b/src/services/hooks/useTableFilter.tsx @@ -31,6 +31,8 @@ export type UseTableFilterOptions> = { paramMap?: Partial, string>>; /** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */ omitDefaultsInUrl?: boolean; + /** Optional list of state keys that should never be serialized into the URL/query string */ + excludeKeysFromUrl?: Partial<(keyof TableFilterState)[]>; persist?: boolean; storeName?: string; @@ -218,9 +220,12 @@ export function useTableFilter>( ); const extras = useMemo(() => { - const { page, pageSize, ...rest } = state as TableFilterState< - Record - >; + const stateWithExtras = state as TableFilterState>; + const rest = Object.fromEntries( + Object.entries(stateWithExtras).filter( + ([key]) => key !== 'page' && key !== 'pageSize' + ) + ); return rest as TExtra; }, [state]); @@ -240,8 +245,13 @@ export function useTableFilter>( const baseline = options?.omitDefaultsInUrl ? (defaults as Record) : null; + const excludedKeys = new Set( + (options?.excludeKeysFromUrl as string[] | undefined) ?? [] + ); for (const key of Object.keys(source)) { + if (excludedKeys.has(key)) continue; + const value = source[key]; if (value === undefined || value === null) continue; @@ -260,7 +270,13 @@ export function useTableFilter>( if (serialized !== null) params.set(mapped, serialized); } return params; - }, [state, defaults, options?.omitDefaultsInUrl, mapKey]); + }, [ + state, + defaults, + options?.omitDefaultsInUrl, + options?.excludeKeysFromUrl, + mapKey, + ]); /** Build query string (prefixed with '?', or empty string if none) */ const toQueryString = useCallback(() => { diff --git a/src/services/http/client.ts b/src/services/http/client.ts index c70a82ea..cb22c2f4 100644 --- a/src/services/http/client.ts +++ b/src/services/http/client.ts @@ -5,7 +5,7 @@ import { RequestOptions } from '@/services/http/base'; import { redirectToSSO } from '@/lib/auth-helper'; const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ''; -const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 10_000 }); +const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 30_000 }); axiosClient.interceptors.response.use( (response) => response, diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index d2dec108..0fe5562e 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -148,10 +148,16 @@ export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload; export type PurchaseFilter = { poDate: string; category: string[]; + category_labels?: { label: string; value: number }[]; status: string[]; supplier_id?: number; + supplier_label?: string; area_id?: number; + area_label?: string; location_id?: number; + location_label?: string; project_flock_id?: number; + project_flock_label?: string; project_flock_kandang_id?: number; + project_flock_kandang_label?: string; };