cambios en la interfaz de organizaciones
This commit is contained in:
10
apps/api/src/database/migrations/0019_cuddly_cobalt_man.sql
Normal file
10
apps/api/src/database/migrations/0019_cuddly_cobalt_man.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "commune_spokesperson_cedula" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "commune_spokesperson_cedula" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "commune_spokesperson_rif" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "commune_spokesperson_rif" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "commune_email" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "commune_email" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "communal_council_spokesperson_cedula" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "communal_council_spokesperson_cedula" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "communal_council_spokesperson_rif" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "communal_council_spokesperson_rif" DROP NOT NULL;
|
||||
2041
apps/api/src/database/migrations/meta/0019_snapshot.json
Normal file
2041
apps/api/src/database/migrations/meta/0019_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -134,6 +134,13 @@
|
||||
"when": 1771855467870,
|
||||
"tag": "0018_milky_prism",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "7",
|
||||
"when": 1771858973096,
|
||||
"tag": "0019_cuddly_cobalt_man",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -262,4 +262,19 @@ export class CreateTrainingDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
parish: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo1?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo2?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo3?: string;
|
||||
}
|
||||
|
||||
@@ -313,27 +313,41 @@ export class TrainingService {
|
||||
// Handle photo updates/removals
|
||||
const photoFields = ['photo1', 'photo2', 'photo3'] as const;
|
||||
|
||||
// 1. If we have NEW files, they replace any old files or occupy empty slots
|
||||
if (photoPaths.length > 0) {
|
||||
photoPaths.forEach((newPath, idx) => {
|
||||
const fieldName = photoFields[idx];
|
||||
const oldPath = currentRecord[fieldName];
|
||||
if (oldPath && oldPath !== newPath) {
|
||||
this.deleteFile(oldPath);
|
||||
}
|
||||
updateData[fieldName] = newPath;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. If the user explicitly cleared a photo field (updateData.photoX === '')
|
||||
// 1. First, handle explicit deletions (where field is '')
|
||||
photoFields.forEach((field) => {
|
||||
if (updateData[field] === '') {
|
||||
const oldPath = currentRecord[field];
|
||||
if (oldPath) this.deleteFile(oldPath);
|
||||
updateData[field] = null; // Set to null in DB
|
||||
updateData[field] = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. We need to find which slots are currently "available" (null) after deletions
|
||||
// and which ones have existing URLs that we want to keep.
|
||||
|
||||
// Let's determine the final state of the 3 slots.
|
||||
const finalPhotos: (string | null)[] = [
|
||||
updateData.photo1 !== undefined ? updateData.photo1 : currentRecord.photo1,
|
||||
updateData.photo2 !== undefined ? updateData.photo2 : currentRecord.photo2,
|
||||
updateData.photo3 !== undefined ? updateData.photo3 : currentRecord.photo3,
|
||||
];
|
||||
|
||||
// 3. Fill the available (null) slots with NEW photo paths
|
||||
if (photoPaths.length > 0) {
|
||||
let photoPathIdx = 0;
|
||||
for (let i = 0; i < 3 && photoPathIdx < photoPaths.length; i++) {
|
||||
if (!finalPhotos[i]) {
|
||||
finalPhotos[i] = photoPaths[photoPathIdx];
|
||||
photoPathIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign back to updateData
|
||||
updateData.photo1 = finalPhotos[0];
|
||||
updateData.photo2 = finalPhotos[1];
|
||||
updateData.photo3 = finalPhotos[2];
|
||||
|
||||
if (updateTrainingDto.visitDate) {
|
||||
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ import {
|
||||
CardTitle,
|
||||
} from '@repo/shadcn/components/ui/card';
|
||||
import { SelectSearchable } from '@repo/shadcn/components/ui/select-searchable';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const OSP_TYPES = ['EPSIC', 'EPSDC', 'UPF', 'OTROS', 'COOPERATIVA'];
|
||||
const STATUS_OPTIONS = ['ACTIVA', 'INACTIVA'];
|
||||
@@ -160,6 +161,16 @@ export function CreateTrainingForm({
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
// 1. Extrae errors de formState
|
||||
const { formState: { errors } } = form;
|
||||
|
||||
// 2. Crea un efecto para monitorearlos
|
||||
useEffect(() => {
|
||||
if (Object.keys(errors).length > 0) {
|
||||
console.log("Campos con errores:", errors);
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
// Cascading Select Logic
|
||||
const ecoSector = useWatch({ control: form.control, name: 'ecoSector' });
|
||||
const productiveSector = useWatch({
|
||||
@@ -253,6 +264,8 @@ export function CreateTrainingForm({
|
||||
}, [defaultValues]);
|
||||
|
||||
const onSubmit = async (formData: TrainingSchema) => {
|
||||
|
||||
|
||||
const data = new FormData();
|
||||
|
||||
// 1. Definimos las claves que NO queremos enviar en el bucle general
|
||||
@@ -260,9 +273,6 @@ export function CreateTrainingForm({
|
||||
// 'photo1/2/3' son strings (urls viejas) que no queremos reenviar como texto.
|
||||
const excludedKeys = [
|
||||
'files',
|
||||
'photo1',
|
||||
'photo2',
|
||||
'photo3',
|
||||
'coorState',
|
||||
'coorMunicipality',
|
||||
'coorParish',
|
||||
@@ -1039,7 +1049,7 @@ export function CreateTrainingForm({
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full flex flex-col space-y-2">
|
||||
<FormLabel className="font-semibold">
|
||||
Correo Electrónico de la Comuna
|
||||
Correo Electrónico de la Comuna (Opcional)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
@@ -1141,7 +1151,7 @@ export function CreateTrainingForm({
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full flex flex-col space-y-2">
|
||||
<FormLabel className="font-semibold">
|
||||
Correo Electrónico del Consejo Comunal
|
||||
Correo Electrónico del Consejo Comunal (Opcional)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
@@ -1298,7 +1308,7 @@ export function CreateTrainingForm({
|
||||
name="generalObservations"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Observaciones Generales</FormLabel>
|
||||
<FormLabel>Observaciones Generales (Opcional)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field} />
|
||||
</FormControl>
|
||||
@@ -1374,19 +1384,25 @@ export function CreateTrainingForm({
|
||||
multiple
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
const newFiles = Array.from(e.target.files || []);
|
||||
const existingCount = [
|
||||
form.watch('photo1'),
|
||||
form.watch('photo2'),
|
||||
form.watch('photo3'),
|
||||
].filter(Boolean).length;
|
||||
|
||||
if (files.length + existingCount > 3) {
|
||||
alert('Máximo 3 imágenes en total');
|
||||
if (
|
||||
newFiles.length + selectedFiles.length + existingCount >
|
||||
3
|
||||
) {
|
||||
toast.error(`Máximo 3 imágenes en total. Ya tienes ${existingCount} subidas y ${selectedFiles.length} seleccionadas para subir.`)
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
setSelectedFiles(files);
|
||||
|
||||
setSelectedFiles((prev) => [...prev, ...newFiles]);
|
||||
// Reset the input value so the same file can be selected again if removed
|
||||
e.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -1398,13 +1414,37 @@ export function CreateTrainingForm({
|
||||
{selectedFiles.map((file, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="relative aspect-square rounded-md overflow-hidden bg-muted"
|
||||
className="relative aspect-square rounded-md overflow-hidden bg-muted group"
|
||||
>
|
||||
<img
|
||||
src={URL.createObjectURL(file)}
|
||||
alt={`Preview ${idx + 1}`}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedFiles((prev) =>
|
||||
prev.filter((_, i) => i !== idx),
|
||||
);
|
||||
}}
|
||||
className="absolute top-1 right-1 bg-destructive text-destructive-foreground rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,17 @@ import { Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
const UNIT_OPTIONS = [
|
||||
'KG',
|
||||
'TON',
|
||||
'UNID',
|
||||
'LT',
|
||||
'MTS',
|
||||
'QQ',
|
||||
'HM2',
|
||||
'SACOS',
|
||||
];
|
||||
|
||||
// 1. Definimos la estructura de los datos para que TypeScript no se queje
|
||||
interface ProductItem {
|
||||
productName: string;
|
||||
@@ -68,20 +79,21 @@ export function ProductActivityList() {
|
||||
dailyCount: '',
|
||||
weeklyCount: '',
|
||||
monthlyCount: '',
|
||||
internalDistributionZone: '',
|
||||
|
||||
// Internal dist
|
||||
internalState: undefined,
|
||||
internalMunicipality: undefined,
|
||||
internalParish: undefined,
|
||||
internalState: null,
|
||||
internalMunicipality: null,
|
||||
internalParish: null,
|
||||
internalDescription: '',
|
||||
internalQuantity: '',
|
||||
internalUnit: '',
|
||||
|
||||
// External dist
|
||||
externalCountry: '',
|
||||
externalState: undefined,
|
||||
externalMunicipality: undefined,
|
||||
externalParish: undefined,
|
||||
externalState: null,
|
||||
externalMunicipality: null,
|
||||
externalParish: null,
|
||||
externalCity: '',
|
||||
externalDescription: '',
|
||||
externalQuantity: '',
|
||||
@@ -117,16 +129,17 @@ export function ProductActivityList() {
|
||||
dailyCount: '',
|
||||
weeklyCount: '',
|
||||
monthlyCount: '',
|
||||
internalState: undefined,
|
||||
internalMunicipality: undefined,
|
||||
internalParish: undefined,
|
||||
internalDistributionZone: '',
|
||||
internalState: null,
|
||||
internalMunicipality: null,
|
||||
internalParish: null,
|
||||
internalDescription: '',
|
||||
internalQuantity: '',
|
||||
internalUnit: '',
|
||||
externalCountry: '',
|
||||
externalState: undefined,
|
||||
externalMunicipality: undefined,
|
||||
externalParish: undefined,
|
||||
externalState: null,
|
||||
externalMunicipality: null,
|
||||
externalParish: null,
|
||||
externalCity: '',
|
||||
externalDescription: '',
|
||||
externalQuantity: '',
|
||||
@@ -337,17 +350,37 @@ export function ProductActivityList() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Cantidad Numérica (KG, TON, UNID. LT, MTS,QQ, HM2, SACOS)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newItem.externalQuantity}
|
||||
onChange={(e) =>
|
||||
setNewItem({
|
||||
...newItem,
|
||||
externalQuantity: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Label>Cantidad Numérica</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
className="flex-1"
|
||||
value={newItem.externalQuantity}
|
||||
onChange={(e) =>
|
||||
setNewItem({
|
||||
...newItem,
|
||||
externalQuantity: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={newItem.externalUnit}
|
||||
onValueChange={(val) =>
|
||||
setNewItem({ ...newItem, externalUnit: val })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="Unidad" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{UNIT_OPTIONS.map((unit) => (
|
||||
<SelectItem key={unit} value={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,10 +17,28 @@ import {
|
||||
} from '@repo/shadcn/components/ui/table';
|
||||
import { Input } from '@repo/shadcn/input';
|
||||
import { Label } from '@repo/shadcn/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@repo/shadcn/select';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
const UNIT_OPTIONS = [
|
||||
'KG',
|
||||
'TON',
|
||||
'UNID',
|
||||
'LT',
|
||||
'MTS',
|
||||
'QQ',
|
||||
'HM2',
|
||||
'SACOS',
|
||||
];
|
||||
|
||||
export function ProductionList() {
|
||||
const { control, register } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
@@ -32,12 +50,13 @@ export function ProductionList() {
|
||||
rawMaterial: '',
|
||||
supplyType: '',
|
||||
quantity: '',
|
||||
unit: '',
|
||||
});
|
||||
|
||||
const handleAdd = () => {
|
||||
if (newItem.rawMaterial && newItem.quantity) {
|
||||
append({ ...newItem, quantity: Number(newItem.quantity) });
|
||||
setNewItem({ rawMaterial: '', supplyType: '', quantity: '' });
|
||||
setNewItem({ rawMaterial: '', supplyType: '', quantity: '', unit: '' });
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -79,15 +98,35 @@ export function ProductionList() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Cantidad Mensual (Kg, TON, UNID.LT, MTS,QQ,HM2,SACO)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newItem.quantity}
|
||||
onChange={(e) =>
|
||||
setNewItem({ ...newItem, quantity: e.target.value })
|
||||
}
|
||||
placeholder="0"
|
||||
/>
|
||||
<Label>Cantidad Mensual</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
className="flex-1"
|
||||
value={newItem.quantity}
|
||||
onChange={(e) =>
|
||||
setNewItem({ ...newItem, quantity: e.target.value })
|
||||
}
|
||||
placeholder="0"
|
||||
/>
|
||||
<Select
|
||||
value={newItem.unit}
|
||||
onValueChange={(val) =>
|
||||
setNewItem({ ...newItem, unit: val })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="Unidad" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{UNIT_OPTIONS.map((unit) => (
|
||||
<SelectItem key={unit} value={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
@@ -134,13 +173,17 @@ export function ProductionList() {
|
||||
{/* @ts-ignore */}
|
||||
{field.supplyType}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`productionList.${index}.quantity`)}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`productionList.${index}.unit`)}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
{field.quantity}
|
||||
{field.quantity} {field.unit}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
|
||||
@@ -40,11 +40,29 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = (id?: number | undefined) => {
|
||||
window.open(`${apiUrl}/training/export/${id}`, '_blank');
|
||||
};
|
||||
// Mapear roles a minúsculas para comparación segura
|
||||
const userRoles = session?.user?.role?.map((r) => r.rol.toLowerCase()) || [];
|
||||
|
||||
const isAdminOrSuper = userRoles.some((r) =>
|
||||
['superadmin', 'admin'].includes(r),
|
||||
);
|
||||
|
||||
// Soporta tanto 'coordinator' como 'coordinador'
|
||||
const isCoordinator = userRoles.some(r =>
|
||||
r.includes('coordinator') || r.includes('coordinador')
|
||||
);
|
||||
|
||||
const role = session?.user.role[0]?.rol;
|
||||
const isOtherAuthorized = userRoles.some((r) =>
|
||||
['autoridad', 'manager'].includes(r),
|
||||
);
|
||||
|
||||
// El creador del registro: intentamos createdBy o created_by por si acaso
|
||||
const createdBy = data.createdBy ?? (data as any).created_by;
|
||||
|
||||
// Comparación robusta de IDs
|
||||
const isOwner = createdBy !== undefined &&
|
||||
createdBy !== null &&
|
||||
Number(createdBy) === Number(session?.user?.id);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,10 +82,8 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||
/>
|
||||
|
||||
<div className="flex gap-1">
|
||||
{/* VER DETALLE: superadmin, admin, autoridad, manager */}
|
||||
{['superadmin', 'admin', 'autoridad', 'manager'].includes(
|
||||
role ?? '',
|
||||
) && (
|
||||
{/* VER DETALLE: superadmin, admin, autoridad, manager, or owner coordinator */}
|
||||
{(isAdminOrSuper || isOtherAuthorized || (isCoordinator && isOwner)) && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -86,47 +102,46 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* EDITAR Y ELIMINAR: Solo superadmin y admin */}
|
||||
{['superadmin', 'admin'].includes(role ?? '') && (
|
||||
<>
|
||||
{/* Editar */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
router.push(`/dashboard/formulario/editar/${data.id}`)
|
||||
}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Editar</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{/* EDITAR: Superadmin, admin OR (coordinator if owner) */}
|
||||
{(isAdminOrSuper || (isCoordinator && isOwner)) && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
router.push(`/dashboard/formulario/editar/${data.id}`)
|
||||
}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Editar</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Eliminar */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Eliminar</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
{/* ELIMINAR: Solo superadmin y admin */}
|
||||
{isAdminOrSuper && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Eliminar</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -273,7 +273,7 @@ export function TrainingViewModal({
|
||||
<span className="text-xs font-bold text-muted-foreground block mb-1">
|
||||
DISTRIBUCIÓN INTERNA
|
||||
</span>
|
||||
<p>Cant: {prod.internalQuantity}</p>
|
||||
<p>Cant: {prod.internalQuantity} {prod.internalUnit}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{prod.internalDescription}
|
||||
</p>
|
||||
@@ -284,7 +284,7 @@ export function TrainingViewModal({
|
||||
<span className="text-xs font-bold text-muted-foreground block mb-1">
|
||||
EXPORTACIÓN ({prod.externalCountry})
|
||||
</span>
|
||||
<p>Cant: {prod.externalQuantity}</p>
|
||||
<p>Cant: {prod.externalQuantity} {prod.externalUnit}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{prod.externalDescription}
|
||||
</p>
|
||||
@@ -360,7 +360,7 @@ export function TrainingViewModal({
|
||||
{mat.supplyType}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary">Cant: {mat.quantity}</Badge>
|
||||
<Badge variant="secondary">Cant: {mat.quantity} {mat.unit}</Badge>
|
||||
</div>
|
||||
))}
|
||||
{(!data.productionList ||
|
||||
|
||||
@@ -11,15 +11,18 @@ const productItemSchema = z.object({
|
||||
|
||||
// Distribución Interna
|
||||
internalDistributionZone: z.string().optional(),
|
||||
internalQuantity: z.coerce.string().or(z.number()).optional(),
|
||||
internalUnit: z.string().optional(),
|
||||
|
||||
// Distribución Externa
|
||||
externalCountry: z.string().optional(),
|
||||
externalState: z.number().optional(),
|
||||
externalMunicipality: z.number().optional(),
|
||||
externalParish: z.number().optional(),
|
||||
externalState: z.number().optional().nullable(),
|
||||
externalMunicipality: z.number().optional().nullable(),
|
||||
externalParish: z.number().optional().nullable(),
|
||||
externalCity: z.string().optional(),
|
||||
externalDescription: z.string().optional(),
|
||||
externalQuantity: z.coerce.string().or(z.number()).optional(),
|
||||
externalUnit: z.string().optional(),
|
||||
|
||||
// Mano de obra
|
||||
womenCount: z.coerce.string().or(z.number()).optional(),
|
||||
@@ -30,6 +33,7 @@ const productionItemSchema = z.object({
|
||||
rawMaterial: z.string(),
|
||||
supplyType: z.string().optional(),
|
||||
quantity: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
|
||||
unit: z.string().optional(),
|
||||
});
|
||||
|
||||
const equipmentItemSchema = z.object({
|
||||
@@ -152,6 +156,7 @@ export const trainingSchema = z.object({
|
||||
photo1: z.string().optional().nullable(),
|
||||
photo2: z.string().optional().nullable(),
|
||||
photo3: z.string().optional().nullable(),
|
||||
createdBy: z.number().optional().nullable(),
|
||||
});
|
||||
|
||||
export type TrainingSchema = z.infer<typeof trainingSchema>;
|
||||
|
||||
Reference in New Issue
Block a user