feat: add CustomButton component and implement conditional UI rendering based on user permissions and document status
This commit is contained in:
parent
0af7da47a9
commit
40e43389df
119
src/components/shared/CustomButton.tsx
Normal file
119
src/components/shared/CustomButton.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import type { ReactElement, ButtonHTMLAttributes } from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppTheme } from '@/hooks/useAppTheme';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
const customButtonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-xs font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer outline-none active:scale-[0.98]',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: '', // Handled by style prop for theme colors
|
||||
secondary: '', // Handled by style prop for theme colors
|
||||
success: 'bg-emerald-600 text-white hover:bg-emerald-700',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700',
|
||||
warning: 'bg-amber-500 text-white hover:bg-amber-600',
|
||||
outline: 'border border-gray-200 bg-white text-gray-700 hover:bg-gray-50',
|
||||
ghost: 'bg-transparent text-gray-600 hover:bg-gray-100',
|
||||
link: 'bg-transparent text-primary underline-offset-4 hover:underline p-0 h-auto',
|
||||
},
|
||||
size: {
|
||||
xs: 'h-7 px-2 text-[10px]',
|
||||
sm: 'h-8 px-3',
|
||||
md: 'h-9 px-4',
|
||||
lg: 'h-10 px-6 text-sm',
|
||||
xl: 'h-12 px-8 text-base',
|
||||
icon: 'h-9 w-9 p-0',
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full',
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface CustomButtonProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof customButtonVariants> {
|
||||
isLoading?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CustomButton = ({
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
fullWidth,
|
||||
className,
|
||||
disabled,
|
||||
isLoading,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
style,
|
||||
...props
|
||||
}: CustomButtonProps): ReactElement => {
|
||||
const { primaryColor, secondaryColor } = useAppTheme();
|
||||
|
||||
|
||||
const getThemeStyles = () => {
|
||||
if (variant === 'secondary') {
|
||||
return {
|
||||
backgroundColor: secondaryColor,
|
||||
color: primaryColor,
|
||||
border: `1px solid ${primaryColor}20`,
|
||||
};
|
||||
}
|
||||
if (variant === 'primary' || !variant) {
|
||||
return {
|
||||
backgroundColor: primaryColor,
|
||||
color: secondaryColor,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
disabled={disabled || isLoading}
|
||||
className={cn(customButtonVariants({ variant, size, fullWidth, className }))}
|
||||
style={{
|
||||
...getThemeStyles(),
|
||||
...style,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!disabled && !isLoading) {
|
||||
if (variant === 'primary' || !variant) {
|
||||
e.currentTarget.style.filter = 'brightness(1.1)';
|
||||
} else if (variant === 'secondary') {
|
||||
e.currentTarget.style.backgroundColor = `${primaryColor}10`;
|
||||
}
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!disabled && !isLoading) {
|
||||
const styles = getThemeStyles() as any;
|
||||
if (styles.backgroundColor) e.currentTarget.style.backgroundColor = styles.backgroundColor;
|
||||
if (styles.color) e.currentTarget.style.color = styles.color;
|
||||
e.currentTarget.style.filter = 'none';
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{leftIcon && <span className="shrink-0">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="shrink-0">{rightIcon}</span>}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@ -11,7 +11,8 @@ import {
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Modal } from "./Modal";
|
||||
import { Modal, CustomButton } from "./";
|
||||
import { useAppTheme } from "@/hooks/useAppTheme";
|
||||
import fileAttachmentService, { type FileAttachment } from "@/services/file-attachment-service";
|
||||
import { DeleteConfirmationModal } from "./DeleteConfirmationModal";
|
||||
|
||||
@ -28,7 +29,8 @@ export const FileShareModal: React.FC<FileShareModalProps> = ({
|
||||
}) => {
|
||||
const [expiresInHours, setExpiresInHours] = useState<number>(24);
|
||||
const [maxDownloads, setMaxDownloads] = useState<number | "">("");
|
||||
const [permissions, setPermissions] = useState<"view" | "download">("download");
|
||||
const permissions = "download";
|
||||
const { primaryColor } = useAppTheme();
|
||||
|
||||
const [isSharing, setIsSharing] = useState(false);
|
||||
const [shareData, setShareData] = useState<{ url: string; token: string; id: string } | null>(null);
|
||||
@ -124,13 +126,18 @@ export const FileShareModal: React.FC<FileShareModalProps> = ({
|
||||
{[1, 24, 72, 168].map((h) => (
|
||||
<button
|
||||
key={h}
|
||||
type="button"
|
||||
onClick={() => setExpiresInHours(h)}
|
||||
className={cn(
|
||||
"py-2 text-xs font-medium rounded-lg border transition-all",
|
||||
"py-2 text-xs font-medium rounded-lg border transition-all cursor-pointer",
|
||||
expiresInHours === h
|
||||
? "bg-[#084cc8] border-[#084cc8] text-white shadow-sm"
|
||||
: "border-[rgba(0,0,0,0.08)] text-[#475569] hover:border-[#084cc8]/30 hover:bg-gray-50"
|
||||
? "text-white shadow-sm"
|
||||
: "border-[rgba(0,0,0,0.08)] text-[#475569] hover:bg-gray-50"
|
||||
)}
|
||||
style={expiresInHours === h ? {
|
||||
backgroundColor: primaryColor,
|
||||
borderColor: primaryColor
|
||||
} : {}}
|
||||
>
|
||||
{h === 1 ? "1 Hr" : h === 24 ? "1 Day" : h === 72 ? "3 Days" : "7 Days"}
|
||||
</button>
|
||||
@ -150,7 +157,11 @@ export const FileShareModal: React.FC<FileShareModalProps> = ({
|
||||
value={maxDownloads}
|
||||
onChange={(e) => setMaxDownloads(e.target.value === "" ? "" : Number(e.target.value))}
|
||||
placeholder="Unlimited"
|
||||
className="w-full h-10 border border-[rgba(0,0,0,0.12)] rounded-lg px-3 text-sm focus:outline-none focus:ring-1 focus:ring-[#084cc8] focus:border-[#084cc8]"
|
||||
className="w-full h-10 border border-[rgba(0,0,0,0.12)] rounded-lg px-3 text-sm focus:outline-none focus:ring-1 transition-all"
|
||||
style={{
|
||||
'--tw-ring-color': primaryColor,
|
||||
borderColor: 'rgba(0,0,0,0.12)'
|
||||
} as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -160,34 +171,25 @@ export const FileShareModal: React.FC<FileShareModalProps> = ({
|
||||
<Shield className="w-3 h-3" />
|
||||
Permission
|
||||
</label>
|
||||
<select
|
||||
value={permissions}
|
||||
onChange={(e) => setPermissions(e.target.value as any)}
|
||||
className="w-full h-10 border border-[rgba(0,0,0,0.12)] rounded-lg px-3 text-sm focus:outline-none focus:ring-1 focus:ring-[#084cc8] focus:border-[#084cc8]"
|
||||
>
|
||||
<option value="view">View Only</option>
|
||||
<option value="download">Download</option>
|
||||
</select>
|
||||
<div className="w-full h-10 border border-[rgba(0,0,0,0.08)] bg-gray-50/50 rounded-lg px-3 flex items-center gap-2 text-sm text-[#475569] font-medium">
|
||||
<Download className="w-3.5 h-3.5 text-[#9aa6b2]" />
|
||||
Download
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<CustomButton
|
||||
variant="primary"
|
||||
fullWidth
|
||||
size="lg"
|
||||
onClick={handleCreateShare}
|
||||
disabled={isSharing}
|
||||
className="w-full h-11 bg-[#084cc8] hover:bg-[#0640aa] text-white rounded-xl text-sm font-bold transition-all shadow-lg shadow-[#084cc8]/20 flex items-center justify-center gap-2"
|
||||
isLoading={isSharing}
|
||||
leftIcon={<Share2 className="w-4 h-4" />}
|
||||
className="rounded-xl shadow-lg"
|
||||
style={{ boxShadow: `${primaryColor}33 0px 8px 24px` }}
|
||||
>
|
||||
{isSharing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Creating Link...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share2 className="w-4 h-4" />
|
||||
Generate Secure Link
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
Generate Secure Link
|
||||
</CustomButton>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-2">
|
||||
@ -208,16 +210,20 @@ export const FileShareModal: React.FC<FileShareModalProps> = ({
|
||||
<label className="text-[10px] font-bold text-[#9aa6b2] uppercase tracking-widest">Share URL</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-11 bg-gray-50 border border-[rgba(0,0,0,0.08)] rounded-xl px-3 flex items-center overflow-hidden">
|
||||
<code className="text-[11px] text-[#084cc8] truncate font-mono">
|
||||
<code
|
||||
className="text-[11px] truncate font-mono"
|
||||
style={{ color: primaryColor }}
|
||||
>
|
||||
{shareData.url}
|
||||
</code>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className={cn(
|
||||
"w-11 h-11 rounded-xl flex items-center justify-center transition-all",
|
||||
copied ? "bg-emerald-500 text-white shadow-emerald-500/30" : "bg-[#084cc8] text-white shadow-[#084cc8]/30 hover:scale-105"
|
||||
"w-11 h-11 rounded-xl flex items-center justify-center transition-all cursor-pointer",
|
||||
copied ? "bg-emerald-500 text-white shadow-emerald-500/30" : "text-white hover:scale-105"
|
||||
)}
|
||||
style={!copied ? { backgroundColor: primaryColor, boxShadow: `${primaryColor}4D 0px 4px 12px` } : {}}
|
||||
>
|
||||
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
|
||||
</button>
|
||||
@ -226,19 +232,21 @@ export const FileShareModal: React.FC<FileShareModalProps> = ({
|
||||
|
||||
<div className="pt-2 flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<CustomButton
|
||||
variant="outline"
|
||||
className="flex-1 rounded-xl"
|
||||
onClick={() => setShareData(null)}
|
||||
className="flex-1 h-10 border border-[rgba(0,0,0,0.1)] hover:bg-gray-50 rounded-xl text-xs font-bold text-[#475569] transition-all"
|
||||
>
|
||||
Create Another
|
||||
</button>
|
||||
<button
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
variant="outline"
|
||||
className="px-4 rounded-xl"
|
||||
leftIcon={<ExternalLink className="w-3.5 h-3.5" />}
|
||||
onClick={() => window.open(shareData.url, '_blank')}
|
||||
className="h-10 px-4 flex items-center gap-2 border border-[rgba(0,0,0,0.1)] hover:bg-gray-50 rounded-xl text-xs font-bold text-[#475569] transition-all"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
Test Link
|
||||
</button>
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export { PrimaryButton } from './PrimaryButton';
|
||||
export { SecondaryButton } from './SecondaryButton';
|
||||
export { CustomButton } from './CustomButton';
|
||||
export { ThemeButton } from './ThemeButton';
|
||||
export { ActionDropdown } from './ActionDropdown';
|
||||
export { FormField } from './FormField';
|
||||
|
||||
@ -14,8 +14,8 @@ import type { Department } from "@/types/department";
|
||||
interface TreeItemProps {
|
||||
item: Department;
|
||||
level?: number;
|
||||
onAddSub: (item: Department) => void;
|
||||
onEdit: (item: Department) => void;
|
||||
onAddSub?: (item: Department) => void;
|
||||
onEdit?: (item: Department) => void;
|
||||
onDelete?: (item: Department) => void;
|
||||
}
|
||||
|
||||
@ -135,28 +135,32 @@ const TreeItem = ({
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => onAddSub(item)}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
isPrimaryActive
|
||||
? "hover:bg-white/10 text-white"
|
||||
: "hover:bg-gray-100 text-[#64748B]"
|
||||
}`}
|
||||
title="Add Sub-department"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onEdit(item)}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
isPrimaryActive
|
||||
? "hover:bg-white/10 text-white"
|
||||
: "hover:bg-gray-100 text-[#64748B]"
|
||||
}`}
|
||||
title="Edit"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
{onAddSub && (
|
||||
<button
|
||||
onClick={() => onAddSub(item)}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
isPrimaryActive
|
||||
? "hover:bg-white/10 text-white"
|
||||
: "hover:bg-gray-100 text-[#64748B]"
|
||||
}`}
|
||||
title="Add Sub-department"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{onEdit && (
|
||||
<button
|
||||
onClick={() => onEdit(item)}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
isPrimaryActive
|
||||
? "hover:bg-white/10 text-white"
|
||||
: "hover:bg-gray-100 text-[#64748B]"
|
||||
}`}
|
||||
title="Edit"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<button
|
||||
onClick={() => onDelete(item)}
|
||||
@ -204,8 +208,8 @@ interface DepartmentTreeViewProps {
|
||||
data: Department[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
onAddSub: (item: Department) => void;
|
||||
onEdit: (item: Department) => void;
|
||||
onAddSub?: (item: Department) => void;
|
||||
onEdit?: (item: Department) => void;
|
||||
onDelete?: (item: Department) => void;
|
||||
}
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ export const DepartmentsTable = forwardRef<
|
||||
ref,
|
||||
): ReactElement => {
|
||||
const { primaryColor } = useAppTheme();
|
||||
const { canUpdate } = usePermissions();
|
||||
const { canUpdate, canCreate } = usePermissions();
|
||||
const reduxTenantId = useSelector(
|
||||
(state: RootState) => state.auth.tenantId,
|
||||
);
|
||||
@ -384,14 +384,22 @@ export const DepartmentsTable = forwardRef<
|
||||
data={treeData}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
onAddSub={(item) => {
|
||||
setSelectedDepartment(item);
|
||||
setIsNewModalOpen(true);
|
||||
}}
|
||||
onEdit={(item) => {
|
||||
setSelectedDepartment(item);
|
||||
setIsEditModalOpen(true);
|
||||
}}
|
||||
onAddSub={
|
||||
canCreate("departments")
|
||||
? (item) => {
|
||||
setSelectedDepartment(item);
|
||||
setIsNewModalOpen(true);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onEdit={
|
||||
canUpdate("departments")
|
||||
? (item) => {
|
||||
setSelectedDepartment(item);
|
||||
setIsEditModalOpen(true);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
import { documentService } from "@/services/document-service";
|
||||
import type { DocumentCategory } from "@/types/document";
|
||||
import { showToast } from "@/utils/toast";
|
||||
import { ArrowLeft, FileText } from "lucide-react";
|
||||
import { FileText } from "lucide-react";
|
||||
import { moduleService } from "@/services/module-service";
|
||||
import type { MyModule } from "@/types/module";
|
||||
|
||||
@ -158,21 +158,21 @@ const EditDocument = (): ReactElement => {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[#0f1724]">
|
||||
Edit Document Metadata
|
||||
Edit Document
|
||||
</h3>
|
||||
<p className="text-xs text-[#6b7280] mt-1">
|
||||
Updates will apply to the current version of the document.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
{/* <button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
className="inline-flex items-center gap-1.5 text-xs text-[#475569] hover:text-[#0f1724]"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
Back
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-x-4">
|
||||
|
||||
@ -6,8 +6,9 @@ import {
|
||||
FormSelect,
|
||||
Modal,
|
||||
PrimaryButton,
|
||||
RichTextEditor,
|
||||
SecondaryButton,
|
||||
CustomButton,
|
||||
RichTextEditor,
|
||||
type Column,
|
||||
} from "@/components/shared";
|
||||
import {
|
||||
@ -502,76 +503,65 @@ const ViewDocument = (): ReactElement => {
|
||||
<div className="flex items-center gap-2">
|
||||
{document?.status === "draft" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md border border-[#e2e8f0] text-[#475569] bg-white text-xs font-medium hover:bg-[#f8fafc] transition-colors"
|
||||
<CustomButton
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/tenant/documents/edit/${id}`)}
|
||||
>
|
||||
Edit Metadata
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md text-white text-xs font-medium transition-colors hover:opacity-90"
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
Edit Document
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
variant="primary"
|
||||
onClick={() => void openActionModal("submit")}
|
||||
>
|
||||
{ACTION_LABELS["submit"]}
|
||||
</button>
|
||||
</CustomButton>
|
||||
</div>
|
||||
)}
|
||||
{document?.status === "in_review" && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md text-white text-xs font-medium opacity-90 hover:opacity-100"
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
<div className="flex items-center gap-2">
|
||||
<CustomButton
|
||||
variant="primary"
|
||||
onClick={() => void openWorkflowTracker()}
|
||||
>
|
||||
Workflow Status
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-emerald-600 text-white text-xs font-medium hover:bg-emerald-700"
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
variant="success"
|
||||
onClick={() => void openActionModal("approve")}
|
||||
>
|
||||
{ACTION_LABELS["approve"]}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-red-600 text-white text-xs font-medium hover:bg-red-700"
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
variant="danger"
|
||||
onClick={() => void openActionModal("reject")}
|
||||
>
|
||||
{ACTION_LABELS["reject"]}
|
||||
</button>
|
||||
</>
|
||||
</CustomButton>
|
||||
</div>
|
||||
)}
|
||||
{document?.status === "approved" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md text-white text-xs font-medium hover:opacity-90"
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
<CustomButton
|
||||
variant="primary"
|
||||
onClick={() => void openActionModal("effective")}
|
||||
>
|
||||
{ACTION_LABELS["effective"]}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-red-600 text-white text-xs font-medium hover:bg-red-700"
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
variant="danger"
|
||||
onClick={() => void openActionModal("reject")}
|
||||
>
|
||||
{ACTION_LABELS["reject"]}
|
||||
</button>
|
||||
</CustomButton>
|
||||
</div>
|
||||
)}
|
||||
{document?.status === "effective" && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md border border-red-200 text-red-600 bg-red-50 text-xs font-medium hover:bg-red-100"
|
||||
<CustomButton
|
||||
variant="danger"
|
||||
onClick={() => void openActionModal("obsolete")}
|
||||
>
|
||||
{ACTION_LABELS["obsolete"]}
|
||||
</button>
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
@ -597,9 +587,12 @@ const ViewDocument = (): ReactElement => {
|
||||
<span className="px-2 py-1 rounded-full bg-amber-100 text-amber-700 text-[11px] font-medium">
|
||||
{document?.document_type || "-"}
|
||||
</span>
|
||||
<span
|
||||
<span
|
||||
className="px-2 py-1 rounded-full text-[11px] font-medium"
|
||||
style={{ backgroundColor: `${primaryColor}1A`, color: primaryColor }}
|
||||
style={{
|
||||
backgroundColor: `${primaryColor}1A`,
|
||||
color: primaryColor,
|
||||
}}
|
||||
>
|
||||
v{document?.current_version || "-"}
|
||||
</span>
|
||||
@ -616,9 +609,12 @@ const ViewDocument = (): ReactElement => {
|
||||
<button
|
||||
type="button"
|
||||
className={`text-sm pb-2 transition-colors`}
|
||||
style={{
|
||||
style={{
|
||||
color: activeTab === "overview" ? primaryColor : "#6b7280",
|
||||
borderBottom: activeTab === "overview" ? `2px solid ${primaryColor}` : "none"
|
||||
borderBottom:
|
||||
activeTab === "overview"
|
||||
? `2px solid ${primaryColor}`
|
||||
: "none",
|
||||
}}
|
||||
onClick={() => setActiveTab("overview")}
|
||||
>
|
||||
@ -627,9 +623,13 @@ const ViewDocument = (): ReactElement => {
|
||||
<button
|
||||
type="button"
|
||||
className={`text-sm pb-2 transition-colors`}
|
||||
style={{
|
||||
color: activeTab === "version-history" ? primaryColor : "#6b7280",
|
||||
borderBottom: activeTab === "version-history" ? `2px solid ${primaryColor}` : "none"
|
||||
style={{
|
||||
color:
|
||||
activeTab === "version-history" ? primaryColor : "#6b7280",
|
||||
borderBottom:
|
||||
activeTab === "version-history"
|
||||
? `2px solid ${primaryColor}`
|
||||
: "none",
|
||||
}}
|
||||
onClick={() => setActiveTab("version-history")}
|
||||
>
|
||||
@ -638,9 +638,13 @@ const ViewDocument = (): ReactElement => {
|
||||
<button
|
||||
type="button"
|
||||
className={`text-sm pb-2 transition-colors`}
|
||||
style={{
|
||||
color: activeTab === "workflow-history" ? primaryColor : "#6b7280",
|
||||
borderBottom: activeTab === "workflow-history" ? `2px solid ${primaryColor}` : "none"
|
||||
style={{
|
||||
color:
|
||||
activeTab === "workflow-history" ? primaryColor : "#6b7280",
|
||||
borderBottom:
|
||||
activeTab === "workflow-history"
|
||||
? `2px solid ${primaryColor}`
|
||||
: "none",
|
||||
}}
|
||||
onClick={() => setActiveTab("workflow-history")}
|
||||
>
|
||||
@ -748,7 +752,8 @@ const ViewDocument = (): ReactElement => {
|
||||
Update Document Content
|
||||
</h4>
|
||||
<p className="text-xs text-[#6b7280] mt-1">
|
||||
Modify the document content or load it from an existing file attachment.
|
||||
Modify the document content or load it from an
|
||||
existing file attachment.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -764,7 +769,8 @@ const ViewDocument = (): ReactElement => {
|
||||
Load Content From File
|
||||
</span>
|
||||
<p className="text-xs text-[#6b7280]">
|
||||
Extract text directly from an uploaded PDF or Word document.
|
||||
Extract text directly from an uploaded PDF
|
||||
or Word document.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -790,22 +796,38 @@ const ViewDocument = (): ReactElement => {
|
||||
{versionSelectedFileId && (
|
||||
<div className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 p-3 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-[11px]">
|
||||
<div>
|
||||
<span className="text-gray-400">File Name:</span>{" "}
|
||||
<span className="text-[#0f1724] font-medium">{versionFileName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">File Size:</span>{" "}
|
||||
<span className="text-gray-400">
|
||||
File Name:
|
||||
</span>{" "}
|
||||
<span className="text-[#0f1724] font-medium">
|
||||
{versionFileSize ? `${(versionFileSize / 1024 / 1024).toFixed(2)} MB` : "-"}
|
||||
{versionFileName}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">MIME Type:</span>{" "}
|
||||
<span className="text-[#0f1724] font-medium">{versionMimeType}</span>
|
||||
<span className="text-gray-400">
|
||||
File Size:
|
||||
</span>{" "}
|
||||
<span className="text-[#0f1724] font-medium">
|
||||
{versionFileSize
|
||||
? `${(versionFileSize / 1024 / 1024).toFixed(2)} MB`
|
||||
: "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Checksum:</span>{" "}
|
||||
<span className="text-[#0f1724] font-mono">{versionFileHash?.substring(0, 12)}...</span>
|
||||
<span className="text-gray-400">
|
||||
MIME Type:
|
||||
</span>{" "}
|
||||
<span className="text-[#0f1724] font-medium">
|
||||
{versionMimeType}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">
|
||||
Checksum:
|
||||
</span>{" "}
|
||||
<span className="text-[#0f1724] font-mono">
|
||||
{versionFileHash?.substring(0, 12)}...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -835,10 +857,22 @@ const ViewDocument = (): ReactElement => {
|
||||
label="Change Reason"
|
||||
required
|
||||
options={[
|
||||
{ value: "minor_edit", label: "minor_edit" },
|
||||
{ value: "correction", label: "correction" },
|
||||
{ value: "regulatory_update", label: "regulatory_update" },
|
||||
{ value: "major_rewrite", label: "major_rewrite" },
|
||||
{
|
||||
value: "minor_edit",
|
||||
label: "minor_edit",
|
||||
},
|
||||
{
|
||||
value: "correction",
|
||||
label: "correction",
|
||||
},
|
||||
{
|
||||
value: "regulatory_update",
|
||||
label: "regulatory_update",
|
||||
},
|
||||
{
|
||||
value: "major_rewrite",
|
||||
label: "major_rewrite",
|
||||
},
|
||||
]}
|
||||
value={newVersionChangeReason}
|
||||
onValueChange={(val) => {
|
||||
@ -853,7 +887,8 @@ const ViewDocument = (): ReactElement => {
|
||||
error={versionErrors.change_reason}
|
||||
/>
|
||||
<p className="text-[10px] text-[#6b7280]">
|
||||
Select a reason category such as minor_edit, correction, etc.
|
||||
Select a reason category such as minor_edit,
|
||||
correction, etc.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -864,16 +899,22 @@ const ViewDocument = (): ReactElement => {
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsMajorVersion(!isMajorVersion)}
|
||||
onClick={() =>
|
||||
setIsMajorVersion(!isMajorVersion)
|
||||
}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none",
|
||||
isMajorVersion ? "bg-[#112868]" : "bg-gray-200"
|
||||
isMajorVersion
|
||||
? "bg-[#112868]"
|
||||
: "bg-gray-200",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
|
||||
isMajorVersion ? "translate-x-4" : "translate-x-0"
|
||||
isMajorVersion
|
||||
? "translate-x-4"
|
||||
: "translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
@ -881,15 +922,24 @@ const ViewDocument = (): ReactElement => {
|
||||
|
||||
<div className="h-10 px-3 flex items-center justify-between bg-white border border-[rgba(0,0,0,0.08)] rounded-md shadow-sm">
|
||||
<span className="text-[11px] font-medium text-[#475569]">
|
||||
{isMajorVersion ? "Major" : "Minor"} Increment
|
||||
{isMajorVersion ? "Major" : "Minor"}{" "}
|
||||
Increment
|
||||
</span>
|
||||
<span className="text-[11px] font-bold text-[#0f1724]">
|
||||
{`${document?.current_version || "0.0"} to ${(() => {
|
||||
if (!document?.current_version) return isMajorVersion ? "1.0" : "0.1";
|
||||
const parts = document.current_version.replace("v", "").split(".");
|
||||
if (!document?.current_version)
|
||||
return isMajorVersion ? "1.0" : "0.1";
|
||||
const parts = document.current_version
|
||||
.replace("v", "")
|
||||
.split(".");
|
||||
let major = parseInt(parts[0]) || 0;
|
||||
let minor = parseInt(parts[1]) || 0;
|
||||
if (isMajorVersion) { major++; minor = 0; } else { minor++; }
|
||||
if (isMajorVersion) {
|
||||
major++;
|
||||
minor = 0;
|
||||
} else {
|
||||
minor++;
|
||||
}
|
||||
return `${major}.${minor}`;
|
||||
})()}`}
|
||||
</span>
|
||||
@ -897,8 +947,7 @@ const ViewDocument = (): ReactElement => {
|
||||
<p className="text-[10px] text-[#6b7280] leading-tight">
|
||||
{isMajorVersion
|
||||
? "Incrementing to a major version indicates significant changes or a full document overhaul."
|
||||
: `Turn on major versioning to increment to v${(parseInt((document?.current_version || "0.0").split(".")[0]) || 0) + 1}.0 instead of a minor revision.`
|
||||
}
|
||||
: `Turn on major versioning to increment to v${(parseInt((document?.current_version || "0.0").split(".")[0]) || 0) + 1}.0 instead of a minor revision.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -917,7 +966,6 @@ const ViewDocument = (): ReactElement => {
|
||||
className="w-full px-3 py-2 border border-[rgba(0,0,0,0.08)] rounded-lg text-sm focus:ring-1 focus:ring-[#112868]/20 focus:outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="p-6 py-4 border-t border-[rgba(0,0,0,0.08)] bg-gray-50/50 flex justify-end gap-3">
|
||||
@ -931,7 +979,9 @@ const ViewDocument = (): ReactElement => {
|
||||
disabled={isVersionSaving}
|
||||
className="px-8"
|
||||
>
|
||||
{isVersionSaving ? "Creating..." : "Save New Version"}
|
||||
{isVersionSaving
|
||||
? "Creating..."
|
||||
: "Save New Version"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
@ -942,8 +992,12 @@ const ViewDocument = (): ReactElement => {
|
||||
{/* Version Context Card */}
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden">
|
||||
<div className="p-5 border-b border-[rgba(0,0,0,0.08)]">
|
||||
<h4 className="text-sm font-bold text-[#0f1724]">Version Context</h4>
|
||||
<p className="text-[11px] text-[#6b7280] mt-0.5">Reference information for current revision.</p>
|
||||
<h4 className="text-sm font-bold text-[#0f1724]">
|
||||
Version Context
|
||||
</h4>
|
||||
<p className="text-[11px] text-[#6b7280] mt-0.5">
|
||||
Reference information for current revision.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-5 pt-4 space-y-4">
|
||||
<div className="inline-flex items-center px-2.5 py-1 rounded-md bg-[#084cc8]/5 text-[#084cc8] text-[11px] font-bold border border-[#084cc8]/10 w-full justify-center">
|
||||
@ -952,24 +1006,56 @@ const ViewDocument = (): ReactElement => {
|
||||
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: "Document Number", value: document?.document_number },
|
||||
{ label: "Current Version", value: `v${document?.current_version || "-"}` },
|
||||
{ label: "Next Version", value: (() => {
|
||||
if (!document?.current_version) return isMajorVersion ? "v1.0" : "v0.1";
|
||||
const parts = document.current_version.replace("v", "").split(".");
|
||||
{
|
||||
label: "Document Number",
|
||||
value: document?.document_number,
|
||||
},
|
||||
{
|
||||
label: "Current Version",
|
||||
value: `v${document?.current_version || "-"}`,
|
||||
},
|
||||
{
|
||||
label: "Next Version",
|
||||
value: (() => {
|
||||
if (!document?.current_version)
|
||||
return isMajorVersion ? "v1.0" : "v0.1";
|
||||
const parts = document.current_version
|
||||
.replace("v", "")
|
||||
.split(".");
|
||||
let major = parseInt(parts[0]) || 0;
|
||||
let minor = parseInt(parts[1]) || 0;
|
||||
if (isMajorVersion) { major++; minor = 0; } else { minor++; }
|
||||
if (isMajorVersion) {
|
||||
major++;
|
||||
minor = 0;
|
||||
} else {
|
||||
minor++;
|
||||
}
|
||||
return `v${major}.${minor}`;
|
||||
})()
|
||||
})(),
|
||||
},
|
||||
{
|
||||
label: "Document Type",
|
||||
value: document?.document_type,
|
||||
},
|
||||
{
|
||||
label: "Owner",
|
||||
value: document?.owner?.name || "-",
|
||||
},
|
||||
{
|
||||
label: "Department",
|
||||
value: document?.department || "-",
|
||||
},
|
||||
{ label: "Document Type", value: document?.document_type },
|
||||
{ label: "Owner", value: document?.owner?.name || "-" },
|
||||
{ label: "Department", value: document?.department || "-" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between py-2 border-b border-gray-50 last:border-0">
|
||||
<span className="text-[11px] text-[#6b7280] font-medium">{item.label}</span>
|
||||
<span className="text-[11px] text-[#0f1724] font-bold">{item.value}</span>
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between py-2 border-b border-gray-50 last:border-0"
|
||||
>
|
||||
<span className="text-[11px] text-[#6b7280] font-medium">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="text-[11px] text-[#0f1724] font-bold">
|
||||
{item.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -979,9 +1065,12 @@ const ViewDocument = (): ReactElement => {
|
||||
{/* Review Notes Card */}
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm p-5 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-[#0f1724]">Review Notes</h4>
|
||||
<h4 className="text-sm font-bold text-[#0f1724]">
|
||||
Review Notes
|
||||
</h4>
|
||||
<p className="text-[11px] text-[#6b7280] mt-0.5 leading-relaxed">
|
||||
Creating a new draft revision will automatically trigger the following system actions:
|
||||
Creating a new draft revision will automatically
|
||||
trigger the following system actions:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -989,11 +1078,16 @@ const ViewDocument = (): ReactElement => {
|
||||
{[
|
||||
"Status resets to Draft",
|
||||
"Workflow restarts",
|
||||
"Version history retained"
|
||||
"Version history retained",
|
||||
].map((note, i) => (
|
||||
<div key={i} className="flex items-center gap-2 px-3 py-2 bg-gray-50 rounded-lg border border-gray-100/50">
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-gray-50 rounded-lg border border-gray-100/50"
|
||||
>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-[#112868]/40" />
|
||||
<span className="text-[11px] font-semibold text-[#475569]">{note}</span>
|
||||
<span className="text-[11px] font-semibold text-[#475569]">
|
||||
{note}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -1354,7 +1448,9 @@ const ViewDocument = (): ReactElement => {
|
||||
<div className="p-4 border border-amber-200 bg-amber-50 rounded-lg animate-in fade-in slide-in-from-top-2">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<h4 className="text-xs font-bold text-amber-900 uppercase tracking-wider">
|
||||
Executing Action: {selectedWorkflowAction.action||selectedWorkflowAction.name}
|
||||
Executing Action:{" "}
|
||||
{selectedWorkflowAction.action ||
|
||||
selectedWorkflowAction.name}
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user