170 lines
6.4 KiB
TypeScript
170 lines
6.4 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { getIconComponent } from '@/components/admin/MenuIconPicker';
|
|
|
|
interface CreateDashboardModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (payload: { name: string; icon: string; is_personal: boolean }) => Promise<void> | void;
|
|
defaultName?: string;
|
|
defaultIcon?: string;
|
|
submitting?: boolean;
|
|
}
|
|
|
|
const ICON_PRESETS = [
|
|
'ClipboardList', 'BarChart3', 'TrendingUp', 'TrendingDown',
|
|
'Package', 'Truck', 'Factory', 'Compass',
|
|
'Map', 'Wrench', 'Settings', 'Folder',
|
|
'Boxes', 'Users', 'Calendar', 'LayoutDashboard',
|
|
];
|
|
|
|
export function CreateDashboardModal({
|
|
open,
|
|
onClose,
|
|
onSubmit,
|
|
defaultName = '',
|
|
defaultIcon = 'ClipboardList',
|
|
submitting = false,
|
|
}: CreateDashboardModalProps) {
|
|
const [name, setName] = useState(defaultName);
|
|
const [icon, setIcon] = useState(defaultIcon);
|
|
const [isPersonal, setIsPersonal] = useState(false);
|
|
const nameRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setName(defaultName);
|
|
setIcon(defaultIcon);
|
|
setIsPersonal(false);
|
|
setTimeout(() => nameRef.current?.focus(), 30);
|
|
}
|
|
}, [open, defaultName, defaultIcon]);
|
|
|
|
if (!open) return null;
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const trimmed = name.trim();
|
|
if (!trimmed || submitting) return;
|
|
await onSubmit({ name: trimmed, icon, is_personal: isPersonal });
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40"
|
|
onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
|
>
|
|
<div className="w-[420px] max-w-[92vw] rounded-xl border border-border bg-[var(--v5-surface-solid)] p-5 shadow-[var(--v5-glow-md)]">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h3 className="text-[0.95rem] font-bold text-foreground">새 대시보드 만들기</h3>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="mb-1.5 block text-[0.7rem] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
이름
|
|
</label>
|
|
<input
|
|
ref={nameRef}
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="예: 수주 관리"
|
|
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-[var(--v5-primary)]"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1.5 block text-[0.7rem] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
아이콘
|
|
</label>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{ICON_PRESETS.map((iconName) => {
|
|
const Ico = getIconComponent(iconName);
|
|
const selected = icon === iconName;
|
|
return (
|
|
<button
|
|
key={iconName}
|
|
type="button"
|
|
onClick={() => setIcon(iconName)}
|
|
className={`flex h-9 w-9 items-center justify-center rounded-md border transition-colors ${
|
|
selected
|
|
? 'border-[var(--v5-primary)] bg-[var(--v5-primary)]/10 text-[var(--v5-primary)]'
|
|
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
}`}
|
|
aria-label={`아이콘 ${iconName}`}
|
|
>
|
|
{Ico ? <Ico className="h-4 w-4" /> : null}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1.5 block text-[0.7rem] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
공유 범위
|
|
</label>
|
|
<div className="space-y-1.5">
|
|
<label className="flex cursor-pointer items-start gap-2 rounded-md border border-border p-2.5 hover:bg-accent/40">
|
|
<input
|
|
type="radio"
|
|
name="scope"
|
|
checked={!isPersonal}
|
|
onChange={() => setIsPersonal(false)}
|
|
className="mt-0.5 accent-[var(--v5-primary)]"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium text-foreground">회사 전체 공용</div>
|
|
<div className="text-xs text-muted-foreground">같은 회사 사용자 모두가 볼 수 있습니다 (기본)</div>
|
|
</div>
|
|
</label>
|
|
<label className="flex cursor-pointer items-start gap-2 rounded-md border border-border p-2.5 hover:bg-accent/40">
|
|
<input
|
|
type="radio"
|
|
name="scope"
|
|
checked={isPersonal}
|
|
onChange={() => setIsPersonal(true)}
|
|
className="mt-0.5 accent-[var(--v5-primary)]"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium text-foreground">나만 보기 (개인 대시보드)</div>
|
|
<div className="text-xs text-muted-foreground">내 사이드바에만 표시됩니다</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-1">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={submitting}
|
|
className="rounded-md border border-border bg-background px-3 py-1.5 text-sm hover:bg-accent disabled:opacity-50"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={submitting || !name.trim()}
|
|
className="rounded-md bg-[var(--v5-primary)] px-3 py-1.5 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-50"
|
|
>
|
|
{submitting ? '생성 중...' : '만들기'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|