feat: 레이아웃/출하/생산/회계/대시보드 전반 개선
- HeaderFavoritesBar 대폭 개선 - Sidebar/AuthenticatedLayout 소폭 수정 - ShipmentCreate, VehicleDispatch 출하 관련 개선 - WorkOrderCreate/Edit, WorkerScreen 생산 관련 개선 - InspectionCreate 자재 입고검사 개선 - DailyReport, VendorDetail 회계 수정 - CEO 대시보드: CardManagement/DailyProduction/DailyAttendance 섹션 개선 - useCEODashboard, expense transformer 정비 - DocumentViewer, PDF generate route 소폭 수정 - bill-prototype 개발 페이지 추가 - mockData 불필요 데이터 제거
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, FileText, X, Edit, Loader2, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { FileText, X, Edit, Loader2, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
@@ -22,14 +22,13 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { SalesOrderSelectModal } from './SalesOrderSelectModal';
|
||||
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { createWorkOrder, getProcessOptions, searchItemsForWorkOrder, type ProcessOption, type ManualItemOption } from './actions';
|
||||
import { PROCESS_TYPE_LABELS, type ProcessType, type SalesOrder } from './types';
|
||||
import { type SalesOrder } from './types';
|
||||
import { workOrderCreateConfig } from './workOrderConfig';
|
||||
|
||||
import { useDevFill } from '@/components/dev';
|
||||
@@ -44,20 +43,6 @@ interface ManualItem {
|
||||
unit: string;
|
||||
}
|
||||
|
||||
// Validation 에러 타입
|
||||
interface ValidationErrors {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// 필드명 매핑
|
||||
const FIELD_NAME_MAP: Record<string, string> = {
|
||||
selectedOrder: '수주',
|
||||
client: '발주처',
|
||||
projectName: '현장명',
|
||||
processId: '공정',
|
||||
shipmentDate: '출고예정일',
|
||||
};
|
||||
|
||||
type RegistrationMode = 'linked' | 'manual';
|
||||
|
||||
interface FormData {
|
||||
@@ -102,7 +87,7 @@ export function WorkOrderCreate() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAssigneeModalOpen, setIsAssigneeModalOpen] = useState(false);
|
||||
const [assigneeNames, setAssigneeNames] = useState<string[]>([]);
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationErrors>({});
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [processOptions, setProcessOptions] = useState<ProcessOption[]>([]);
|
||||
const [isLoadingProcesses, setIsLoadingProcesses] = useState(true);
|
||||
@@ -114,6 +99,17 @@ export function WorkOrderCreate() {
|
||||
const [isSearchingItems, setIsSearchingItems] = useState(false);
|
||||
const [showItemSearch, setShowItemSearch] = useState(false);
|
||||
|
||||
// 필드 에러 클리어 헬퍼
|
||||
const clearFieldError = useCallback((field: string) => {
|
||||
if (validationErrors[field]) {
|
||||
setValidationErrors(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[field];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}, [validationErrors]);
|
||||
|
||||
// 공정 옵션 로드
|
||||
useEffect(() => {
|
||||
async function loadProcessOptions() {
|
||||
@@ -173,6 +169,7 @@ export function WorkOrderCreate() {
|
||||
orderNo: order.orderNo,
|
||||
itemCount: order.itemCount,
|
||||
});
|
||||
clearFieldError('selectedOrder');
|
||||
};
|
||||
|
||||
// 수주 해제
|
||||
@@ -217,6 +214,7 @@ export function WorkOrderCreate() {
|
||||
setShowItemSearch(false);
|
||||
setItemSearchQuery('');
|
||||
setItemSearchResults([]);
|
||||
clearFieldError('items');
|
||||
};
|
||||
|
||||
// 품목 수량 변경
|
||||
@@ -232,7 +230,7 @@ export function WorkOrderCreate() {
|
||||
// 폼 제출
|
||||
const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => {
|
||||
// Validation 체크
|
||||
const errors: ValidationErrors = {};
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (mode === 'linked') {
|
||||
if (!formData.selectedOrder) {
|
||||
@@ -261,8 +259,8 @@ export function WorkOrderCreate() {
|
||||
// 에러가 있으면 상태 업데이트 후 리턴
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setValidationErrors(errors);
|
||||
// 페이지 상단으로 스크롤
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
const firstError = Object.values(errors)[0];
|
||||
toast.error(firstError);
|
||||
return { success: false, error: '' };
|
||||
}
|
||||
|
||||
@@ -318,35 +316,6 @@ export function WorkOrderCreate() {
|
||||
// 폼 컨텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => (
|
||||
<div className="space-y-6">
|
||||
{/* Validation 에러 표시 */}
|
||||
{Object.keys(validationErrors).length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
<AlertDescription className="text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg">⚠️</span>
|
||||
<div className="flex-1">
|
||||
<strong className="block mb-2">
|
||||
입력 내용을 확인해주세요 ({Object.keys(validationErrors).length}개 오류)
|
||||
</strong>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{Object.entries(validationErrors).map(([field, message]) => {
|
||||
const fieldName = FIELD_NAME_MAP[field] || field;
|
||||
return (
|
||||
<li key={field} className="flex items-start gap-1">
|
||||
<span>•</span>
|
||||
<span>
|
||||
<strong>{fieldName}</strong>: {message}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 등록 방식 */}
|
||||
<section className="bg-white border rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">등록 방식</h3>
|
||||
@@ -381,7 +350,7 @@ export function WorkOrderCreate() {
|
||||
<section className="bg-muted/30 border rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">수주 정보</h3>
|
||||
{!formData.selectedOrder ? (
|
||||
<div className="flex items-center justify-between p-4 bg-white border rounded-lg">
|
||||
<div className={`flex items-center justify-between p-4 bg-white border rounded-lg ${validationErrors.selectedOrder ? 'border-red-500' : ''}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
@@ -448,6 +417,7 @@ export function WorkOrderCreate() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{validationErrors.selectedOrder && <p className="text-sm text-red-500 mt-1">{validationErrors.selectedOrder}</p>}
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -459,21 +429,29 @@ export function WorkOrderCreate() {
|
||||
<Label>발주처 *</Label>
|
||||
<Input
|
||||
value={formData.client}
|
||||
onChange={(e) => setFormData({ ...formData, client: e.target.value })}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, client: e.target.value });
|
||||
clearFieldError('client');
|
||||
}}
|
||||
placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '발주처 입력'}
|
||||
disabled={mode === 'linked'}
|
||||
className="bg-white"
|
||||
className={`bg-white ${validationErrors.client ? 'border-red-500' : ''}`}
|
||||
/>
|
||||
{validationErrors.client && <p className="text-sm text-red-500">{validationErrors.client}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>현장명 *</Label>
|
||||
<Input
|
||||
value={formData.projectName}
|
||||
onChange={(e) => setFormData({ ...formData, projectName: e.target.value })}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, projectName: e.target.value });
|
||||
clearFieldError('projectName');
|
||||
}}
|
||||
placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '현장명 입력'}
|
||||
disabled={mode === 'linked'}
|
||||
className="bg-white"
|
||||
className={`bg-white ${validationErrors.projectName ? 'border-red-500' : ''}`}
|
||||
/>
|
||||
{validationErrors.projectName && <p className="text-sm text-red-500">{validationErrors.projectName}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>수주번호</Label>
|
||||
@@ -506,10 +484,13 @@ export function WorkOrderCreate() {
|
||||
<Label>공정구분 *</Label>
|
||||
<Select
|
||||
value={formData.processId?.toString() || ''}
|
||||
onValueChange={(value) => setFormData({ ...formData, processId: parseInt(value) })}
|
||||
onValueChange={(value) => {
|
||||
setFormData({ ...formData, processId: parseInt(value) });
|
||||
clearFieldError('processId');
|
||||
}}
|
||||
disabled={isLoadingProcesses}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectTrigger className={`bg-white ${validationErrors.processId ? 'border-red-500' : ''}`}>
|
||||
<SelectValue placeholder={isLoadingProcesses ? '로딩 중...' : '공정을 선택하세요'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -520,6 +501,7 @@ export function WorkOrderCreate() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{validationErrors.processId && <p className="text-sm text-red-500">{validationErrors.processId}</p>}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
공정코드: {getSelectedProcessCode()}
|
||||
</p>
|
||||
@@ -529,8 +511,13 @@ export function WorkOrderCreate() {
|
||||
<Label>출고예정일 *</Label>
|
||||
<DatePicker
|
||||
value={formData.shipmentDate}
|
||||
onChange={(date) => setFormData({ ...formData, shipmentDate: date })}
|
||||
onChange={(date) => {
|
||||
setFormData({ ...formData, shipmentDate: date });
|
||||
clearFieldError('shipmentDate');
|
||||
}}
|
||||
className={validationErrors.shipmentDate ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.shipmentDate && <p className="text-sm text-red-500">{validationErrors.shipmentDate}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -717,7 +704,7 @@ export function WorkOrderCreate() {
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
), [mode, formData, validationErrors, processOptions, isLoadingProcesses, assigneeNames, getSelectedProcessCode, manualItems, showItemSearch, itemSearchQuery, itemSearchResults, isSearchingItems]);
|
||||
), [mode, formData, validationErrors, processOptions, isLoadingProcesses, assigneeNames, getSelectedProcessCode, manualItems, showItemSearch, itemSearchQuery, itemSearchResults, isSearchingItems, clearFieldError]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -751,4 +738,4 @@ export function WorkOrderCreate() {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
||||
import { toast } from 'sonner';
|
||||
@@ -52,17 +51,6 @@ interface EditableItem extends WorkOrderItem {
|
||||
editQuantity?: number;
|
||||
}
|
||||
|
||||
// Validation 에러 타입
|
||||
interface ValidationErrors {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// 필드명 매핑
|
||||
const FIELD_NAME_MAP: Record<string, string> = {
|
||||
processId: '공정',
|
||||
scheduledDate: '출고예정일',
|
||||
};
|
||||
|
||||
interface FormData {
|
||||
// 기본 정보 (읽기 전용)
|
||||
client: string;
|
||||
@@ -101,7 +89,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
const [isAssigneeModalOpen, setIsAssigneeModalOpen] = useState(false);
|
||||
const [deleteTargetItemId, setDeleteTargetItemId] = useState<string | null>(null);
|
||||
const [assigneeNames, setAssigneeNames] = useState<string[]>([]);
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationErrors>({});
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [processOptions, setProcessOptions] = useState<ProcessOption[]>([]);
|
||||
@@ -213,7 +201,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
// 폼 제출
|
||||
const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => {
|
||||
// Validation 체크
|
||||
const errors: ValidationErrors = {};
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!formData.processId) {
|
||||
errors.processId = '공정을 선택해주세요';
|
||||
@@ -226,7 +214,8 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
// 에러가 있으면 상태 업데이트 후 리턴
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setValidationErrors(errors);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
const firstError = Object.values(errors)[0];
|
||||
toast.error(firstError);
|
||||
return { success: false, error: '입력 정보를 확인해주세요.' };
|
||||
}
|
||||
|
||||
@@ -344,35 +333,6 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
// 폼 컨텐츠 렌더링 (기획서 4열 그리드)
|
||||
const renderFormContent = useCallback(() => (
|
||||
<div className="space-y-6">
|
||||
{/* Validation 에러 표시 */}
|
||||
{Object.keys(validationErrors).length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
<AlertDescription className="text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg">⚠️</span>
|
||||
<div className="flex-1">
|
||||
<strong className="block mb-2">
|
||||
입력 내용을 확인해주세요 ({Object.keys(validationErrors).length}개 오류)
|
||||
</strong>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{Object.entries(validationErrors).map(([field, message]) => {
|
||||
const fieldName = FIELD_NAME_MAP[field] || field;
|
||||
return (
|
||||
<li key={field} className="flex items-start gap-1">
|
||||
<span>•</span>
|
||||
<span>
|
||||
<strong>{fieldName}</strong>: {message}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 기본 정보 (기획서 4열 구성) */}
|
||||
<section className="bg-amber-50 border border-amber-200 rounded-lg p-4 md:p-6">
|
||||
<h3 className="font-semibold mb-4">기본 정보</h3>
|
||||
@@ -391,10 +351,15 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
<Label className="text-sm text-muted-foreground">공정 *</Label>
|
||||
<Select
|
||||
value={formData.processId?.toString() || ''}
|
||||
onValueChange={(value) => setFormData({ ...formData, processId: parseInt(value) })}
|
||||
onValueChange={(value) => {
|
||||
setFormData({ ...formData, processId: parseInt(value) });
|
||||
if (validationErrors.processId) {
|
||||
setValidationErrors(prev => { const { processId: _, ...rest } = prev; return rest; });
|
||||
}
|
||||
}}
|
||||
disabled={isLoadingProcesses}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectTrigger className={`bg-white ${validationErrors.processId ? 'border-red-500' : ''}`}>
|
||||
<SelectValue placeholder={isLoadingProcesses ? '로딩 중...' : '공정 선택'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -405,6 +370,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{validationErrors.processId && <p className="text-sm text-red-500">{validationErrors.processId}</p>}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">구분</Label>
|
||||
@@ -442,8 +408,15 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
<Label className="text-sm text-muted-foreground">출고예정일 *</Label>
|
||||
<DatePicker
|
||||
value={formData.scheduledDate}
|
||||
onChange={(date) => setFormData({ ...formData, scheduledDate: date })}
|
||||
onChange={(date) => {
|
||||
setFormData({ ...formData, scheduledDate: date });
|
||||
if (validationErrors.scheduledDate) {
|
||||
setValidationErrors(prev => { const { scheduledDate: _, ...rest } = prev; return rest; });
|
||||
}
|
||||
}}
|
||||
className={validationErrors.scheduledDate ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.scheduledDate && <p className="text-sm text-red-500">{validationErrors.scheduledDate}</p>}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">틀수</Label>
|
||||
@@ -671,4 +644,4 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,15 +348,28 @@ export default function WorkerScreen() {
|
||||
// 작업지시별 단계 진행 캐시: { [workOrderId]: StepProgressItem[] }
|
||||
const [stepProgressMap, setStepProgressMap] = useState<Record<string, StepProgressItem[]>>({});
|
||||
|
||||
// 데이터 로드
|
||||
// 데이터 로드 (작업목록 + 공정목록 + 부서목록 병렬)
|
||||
const loadData = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await getMyWorkOrders();
|
||||
if (result.success) {
|
||||
setWorkOrders(result.data);
|
||||
const [workOrderResult, processResult, deptResult] = await Promise.all([
|
||||
getMyWorkOrders(),
|
||||
getProcessList({ size: 100 }),
|
||||
getDepartments(),
|
||||
]);
|
||||
|
||||
if (workOrderResult.success) {
|
||||
setWorkOrders(workOrderResult.data);
|
||||
} else {
|
||||
toast.error(result.error || '작업 목록 조회에 실패했습니다.');
|
||||
toast.error(workOrderResult.error || '작업 목록 조회에 실패했습니다.');
|
||||
}
|
||||
|
||||
if (processResult.success && processResult.data?.items) {
|
||||
setProcessListCache(processResult.data.items);
|
||||
}
|
||||
|
||||
if (deptResult.success) {
|
||||
setDepartmentList(deptResult.data);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
@@ -369,10 +382,6 @@ export default function WorkerScreen() {
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
// 부서 목록 로드
|
||||
getDepartments().then((res) => {
|
||||
if (res.success) setDepartmentList(res.data);
|
||||
});
|
||||
}, [loadData]);
|
||||
|
||||
// 부서 선택 시 해당 부서 사용자 목록 로드
|
||||
@@ -455,21 +464,6 @@ export default function WorkerScreen() {
|
||||
// 공정 목록 캐시
|
||||
const [processListCache, setProcessListCache] = useState<Process[]>([]);
|
||||
|
||||
// 공정 목록 조회 (최초 1회)
|
||||
useEffect(() => {
|
||||
const fetchProcessList = async () => {
|
||||
try {
|
||||
const result = await getProcessList({ size: 100 });
|
||||
if (result.success && result.data?.items) {
|
||||
setProcessListCache(result.data.items);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch process list:', error);
|
||||
}
|
||||
};
|
||||
fetchProcessList();
|
||||
}, []);
|
||||
|
||||
// 활성 공정 목록 (탭용) - 공정관리에서 등록된 활성 공정만
|
||||
const processTabs = useMemo(() => {
|
||||
return processListCache.filter((p) => p.status === '사용중');
|
||||
@@ -1478,6 +1472,9 @@ export default function WorkerScreen() {
|
||||
</div>
|
||||
|
||||
{/* 공정별 탭 (공정관리 API 기반 동적 생성) */}
|
||||
{isLoading ? (
|
||||
<ContentSkeleton type="list" rows={1} />
|
||||
) : (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v)}
|
||||
@@ -1708,11 +1705,12 @@ export default function WorkerScreen() {
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하단 고정 버튼 */}
|
||||
{(hasWipItems || activeProcessSettings.needsWorkLog || activeProcessSettings.hasDocumentTemplate) && (
|
||||
<div className={`fixed bottom-4 left-3 right-3 px-3 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:left-auto md:right-[24px] md:px-6 ${sidebarCollapsed ? 'md:left-[113px]' : 'md:left-[304px]'}`}>
|
||||
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[24px] ${sidebarCollapsed ? 'md:left-[120px]' : 'md:left-[280px]'}`}>
|
||||
<div className="flex gap-2 md:gap-3">
|
||||
{hasWipItems ? (
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user