Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
@@ -69,6 +69,7 @@ function buildSearchFormValues(
|
||||
isDetail: false,
|
||||
isSummary: d.isSummary || "",
|
||||
clsfCd: initialClsfCd,
|
||||
capacityGbn: d.capacityGbn || "ALL",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ function buildSearchFormValues(
|
||||
isDetail: false,
|
||||
isSummary: d?.isSummary || "",
|
||||
clsfCd: initialClsfCd,
|
||||
capacityGbn: d?.capacityGbn || "ALL",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -362,6 +364,26 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mergedProps.capacityGbn && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">구분</Label>
|
||||
<Select
|
||||
value={values.capacityGbn}
|
||||
onValueChange={(value) =>
|
||||
setValues({ ...values, capacityGbn: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='ALL'>전체</SelectItem>
|
||||
<SelectItem value='500톤이상'>500톤이상</SelectItem>
|
||||
<SelectItem value='500톤미만'>500톤미만</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{mergedProps.isDetail && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label cursor-pointer" htmlFor="isDetail">
|
||||
|
||||
@@ -311,15 +311,24 @@ export function VerificationResultDialog({
|
||||
}, [rows, loading]);
|
||||
|
||||
const handleExceptionSubmitClick = useCallback(async () => {
|
||||
if (checkedIndices.size === 0) {
|
||||
alert("작성된 예외 제출 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const gridRows = getGridDataRef.current?.() ?? [];
|
||||
const payload: VerificationExceptionSubmitItem[] = [];
|
||||
checkedIndices.forEach((idx) => {
|
||||
for (const idx of checkedIndices) {
|
||||
const prcsOpnnExpln = String(gridRows[idx]?.prcsOpnnExpln ?? "").trim();
|
||||
if (!prcsOpnnExpln) return;
|
||||
if (!prcsOpnnExpln) {
|
||||
alert("예외 제출 사유를 작성해주세요.");
|
||||
return;
|
||||
}
|
||||
const vrfcSn = Number(rows[idx]?.vrfcSn);
|
||||
if (!Number.isFinite(vrfcSn)) return;
|
||||
if (!Number.isFinite(vrfcSn)) continue;
|
||||
payload.push({ vrfcSn, prcsOpnnExpln });
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.length === 0) {
|
||||
alert("작성된 예외 제출 항목이 없습니다.");
|
||||
return;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
@@ -12,8 +15,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@common/components";
|
||||
|
||||
export interface MainFacilityMappingSaveDialogValues {
|
||||
@@ -79,6 +84,95 @@ function validateContractRange(start: Date | undefined, end: Date | undefined):
|
||||
return null;
|
||||
}
|
||||
|
||||
const CONTRACT_DATE_END_MONTH = new Date(new Date().getFullYear() + 10, 11);
|
||||
|
||||
function isValidDate(date: Date | undefined) {
|
||||
if (!date) return false;
|
||||
return !Number.isNaN(date.getTime());
|
||||
}
|
||||
|
||||
/** 이 다이얼로그 전용 — 연도 드롭다운을 현재+10년까지 허용 */
|
||||
function ContractDatePicker({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
className,
|
||||
}: {
|
||||
value?: Date;
|
||||
onChange: (date: Date | undefined) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [month, setMonth] = useState<Date | undefined>(value);
|
||||
const [displayValue, setDisplayValue] = useState<Date | undefined>(value);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayValue(value);
|
||||
setMonth(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="relative flex gap-2">
|
||||
<Input
|
||||
type="date"
|
||||
value={displayValue ? dayjs(displayValue).format("YYYY-MM-DD") : ""}
|
||||
disabled={disabled}
|
||||
max={dayjs(CONTRACT_DATE_END_MONTH).format("YYYY-MM-DD")}
|
||||
className={`bg-background pr-10 ${className ?? ""}`}
|
||||
onChange={(e) => {
|
||||
const newDate = new Date(e.target.value);
|
||||
setDisplayValue(newDate);
|
||||
if (isValidDate(newDate)) {
|
||||
onChange(newDate);
|
||||
setMonth(newDate);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="white"
|
||||
disabled={disabled}
|
||||
className="absolute right-2 top-1/2 size-7 -translate-y-1/2 rounded-[0.4rem] border-none p-0"
|
||||
>
|
||||
<CalendarIcon className="!size-full" />
|
||||
<span className="sr-only">Select date</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="end"
|
||||
alignOffset={-8}
|
||||
sideOffset={10}
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={value}
|
||||
captionLayout="dropdown"
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
endMonth={CONTRACT_DATE_END_MONTH}
|
||||
onSelect={(newDate) => {
|
||||
onChange(newDate);
|
||||
setDisplayValue(newDate);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MainFacilityMappingSaveDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -220,7 +314,7 @@ export function MainFacilityMappingSaveDialog({
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Label className="text-label text-[1.2rem]">시작일</Label>
|
||||
<InputDatePicker
|
||||
<ContractDatePicker
|
||||
value={prxyCtrtStart}
|
||||
onChange={setPrxyCtrtStart}
|
||||
className="w-full"
|
||||
@@ -229,7 +323,7 @@ export function MainFacilityMappingSaveDialog({
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Label className="text-label text-[1.2rem]">종료일</Label>
|
||||
<InputDatePicker
|
||||
<ContractDatePicker
|
||||
value={prxyCtrtEnd}
|
||||
onChange={setPrxyCtrtEnd}
|
||||
className="w-full"
|
||||
|
||||
@@ -21,6 +21,7 @@ const PublicSewageFacilityTot = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const isSummaryEnabled = values.isSummary !== 'ALL';
|
||||
const isCapacityGbn = values.capacityGbn !== 'ALL'
|
||||
const summaryYearCnt = isSummaryEnabled ? Number(values.isSummary) : undefined;
|
||||
|
||||
const param = {
|
||||
@@ -29,6 +30,7 @@ const PublicSewageFacilityTot = () => {
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
...(isCapacityGbn ? { capacityGbn: values.capacityGbn } : {}),
|
||||
summaryYn: isSummaryEnabled,
|
||||
...(isSummaryEnabled ? { summaryYearCnt } : {}),
|
||||
};
|
||||
@@ -50,7 +52,7 @@ const PublicSewageFacilityTot = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" />
|
||||
<SearchForm isDetail={true} capacityGbn={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" />
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
|
||||
@@ -12,6 +12,7 @@ export type SearchFormValues = {
|
||||
isSummary?: string;
|
||||
/** 분류: 전체(ALL), 입력(INPT), 검증(VRFC) */
|
||||
clsfCd?: SearchFormClsfCd;
|
||||
capacityGbn?: string;
|
||||
};
|
||||
|
||||
export type SearchFormProps = {
|
||||
@@ -24,6 +25,7 @@ export type SearchFormProps = {
|
||||
/** 분류(입력/검증) 셀렉트 표시 */
|
||||
clsf?: boolean;
|
||||
userMenual?: boolean;
|
||||
capacityGbn?: boolean;
|
||||
isDetail?: boolean;
|
||||
isSummary?: boolean;
|
||||
onSubmit?: (values: SearchFormValues) => void;
|
||||
@@ -44,6 +46,7 @@ export const defaultSearchFormProps: Required<
|
||||
fctName: false,
|
||||
clsf: false,
|
||||
userMenual: false,
|
||||
capacityGbn: false,
|
||||
isDetail: false,
|
||||
isSummary: false,
|
||||
isReset: true,
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ export interface SewagePipeMaintainTotalRequest {
|
||||
rowDataYn?: boolean;
|
||||
summaryYn?: boolean;
|
||||
summaryYearCnt?: number;
|
||||
capacityGbn?: string;
|
||||
}
|
||||
|
||||
export interface SewagePipeMaintainRepairTotalItem {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import type { DatesSetArg } from "@fullcalendar/core";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import dayjs from "dayjs";
|
||||
@@ -12,25 +14,48 @@ import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
// FullCalendar는 클라이언트 전용이므로 dynamic import
|
||||
const FullCalendar = dynamic(() => import("@fullcalendar/react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
function getMonthRange(date = dayjs()) {
|
||||
return {
|
||||
schdlBgngYmd: date.startOf("month").format("YYYY-MM-DD"),
|
||||
schdlEndYmd: date.endOf("month").format("YYYY-MM-DD"),
|
||||
};
|
||||
}
|
||||
|
||||
export function ScheduleCalendarPanel() {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const [dateRange, setDateRange] = useState(getMonthRange);
|
||||
|
||||
// TBD: 초기 날짜 범위 설정 로직
|
||||
const today = dayjs();
|
||||
const startOfMonth = today.startOf("month").format("YYYY-MM-DD");
|
||||
const endOfMonth = today.endOf("month").format("YYYY-MM-DD");
|
||||
|
||||
const { data, isLoading } = useSchedules({
|
||||
const { data, isLoading, isFetching } = useSchedules({
|
||||
siteId: SITE_ID,
|
||||
schdlBgngYmd: startOfMonth,
|
||||
schdlEndYmd: endOfMonth,
|
||||
schdlBgngYmd: dateRange.schdlBgngYmd,
|
||||
schdlEndYmd: dateRange.schdlEndYmd,
|
||||
});
|
||||
|
||||
function handleDatesSet(dateInfo: DatesSetArg) {
|
||||
const nextRange = {
|
||||
schdlBgngYmd: dayjs(dateInfo.view.currentStart).format(
|
||||
"YYYY-MM-DD",
|
||||
),
|
||||
schdlEndYmd: dayjs(dateInfo.view.currentEnd)
|
||||
.subtract(1, "day")
|
||||
.format("YYYY-MM-DD"),
|
||||
};
|
||||
|
||||
setDateRange((prev) => {
|
||||
if (
|
||||
prev.schdlBgngYmd === nextRange.schdlBgngYmd &&
|
||||
prev.schdlEndYmd === nextRange.schdlEndYmd
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return nextRange;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
@@ -40,41 +65,39 @@ export function ScheduleCalendarPanel() {
|
||||
>
|
||||
<h3 className="text-2xl font-semibold">일정관리</h3>
|
||||
</div>
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="card-body relative max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
<div className="h-[48rem]">
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin, timeGridPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
locale="ko"
|
||||
headerToolbar={{
|
||||
left: "prev,next",
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek",
|
||||
}}
|
||||
buttonText={{
|
||||
month: "월",
|
||||
week: "주",
|
||||
}}
|
||||
datesSet={handleDatesSet}
|
||||
events={(data ?? []).map((schedule) => ({
|
||||
id: schedule.schdlSn.toString(),
|
||||
title: schedule.schdlTtl,
|
||||
start: schedule.schdlBgngDt,
|
||||
end: schedule.schdlEndDt,
|
||||
allDay: true,
|
||||
}))}
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
{(isLoading || isFetching) && (
|
||||
<div className="bg-background/50 absolute inset-0 flex items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="h-[48rem]">
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin, timeGridPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
locale="ko"
|
||||
headerToolbar={{
|
||||
left: "prev,next",
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek",
|
||||
}}
|
||||
buttonText={{
|
||||
month: "월",
|
||||
week: "주",
|
||||
}}
|
||||
events={data.map((schedule) => ({
|
||||
id: schedule.schdlSn.toString(),
|
||||
title: schedule.schdlTtl,
|
||||
start: schedule.schdlBgngDt,
|
||||
end: schedule.schdlEndDt,
|
||||
// TBD: allDay 판단 기준 (1439분 이상?)
|
||||
allDay: true,
|
||||
}))}
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -83,36 +83,59 @@ export function ScheduleInfoPanel({ params }: ScheduleInfoPanelProps) {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between rounded-sm border border-border p-3">
|
||||
<Badge size="lg" className="font-semibold">
|
||||
입력기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.inptBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.inptEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
{data.inptSeCd &&
|
||||
data.inptBgngYmd &&
|
||||
data.inptEndYmd ? (
|
||||
<>
|
||||
<Badge
|
||||
size="lg"
|
||||
className="font-semibold"
|
||||
>
|
||||
{data.inptSeCd}차 입력기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.inptBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.inptEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="w-full text-center text-lg text-label">
|
||||
등록된 입력 일정이 없습니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-sm border border-border p-3">
|
||||
<Badge
|
||||
size="lg"
|
||||
color="gray"
|
||||
className="font-semibold"
|
||||
>
|
||||
검증기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.vrfcBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.vrfcEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
{data.vrfcSeCd &&
|
||||
data.vrfcBgngYmd &&
|
||||
data.vrfcEndYmd ? (
|
||||
<>
|
||||
<Badge
|
||||
size="lg"
|
||||
color="gray"
|
||||
className="font-semibold"
|
||||
>
|
||||
{data.vrfcSeCd}차 검증기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.vrfcBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.vrfcEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="w-full text-center text-lg text-label">
|
||||
등록된 검증 일정이 없습니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-4 py-4 tracking-tight">
|
||||
|
||||
Reference in New Issue
Block a user