Tabla de inventario agregada
This commit is contained in:
@@ -18,7 +18,7 @@ import { MailModule } from './features/mail/mail.module';
|
|||||||
import { RolesModule } from './features/roles/roles.module';
|
import { RolesModule } from './features/roles/roles.module';
|
||||||
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
||||||
import { SurveysModule } from './features/surveys/surveys.module';
|
import { SurveysModule } from './features/surveys/surveys.module';
|
||||||
|
import {InventoryModule} from './features/inventory/inventory.module'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -58,7 +58,8 @@ import { SurveysModule } from './features/surveys/surveys.module';
|
|||||||
UserRolesModule,
|
UserRolesModule,
|
||||||
ConfigurationsModule,
|
ConfigurationsModule,
|
||||||
SurveysModule,
|
SurveysModule,
|
||||||
LocationModule
|
LocationModule,
|
||||||
|
InventoryModule
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE "products" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"description" text NOT NULL,
|
||||||
|
"price" numeric NOT NULL,
|
||||||
|
"stock" integer NOT NULL,
|
||||||
|
"url_img" text NOT NULL,
|
||||||
|
"user_id" integer,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp (3)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "products" ADD CONSTRAINT "products_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
1441
apps/api/src/database/migrations/meta/0002_snapshot.json
Normal file
1441
apps/api/src/database/migrations/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
|||||||
"when": 1747665408016,
|
"when": 1747665408016,
|
||||||
"tag": "0001_massive_kylun",
|
"tag": "0001_massive_kylun",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1750442271575,
|
||||||
|
"tag": "0002_polite_franklin_richards",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||||
import { Env, validateString } from '@/common/utils';
|
|
||||||
import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import * as schema from 'src/database/index';
|
import * as schema from 'src/database/index';
|
||||||
import { products } from 'src/database/index';
|
import { products } from 'src/database/index';
|
||||||
import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm';
|
import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm';
|
||||||
import * as bcrypt from 'bcryptjs';
|
|
||||||
import { CreateProductDto } from './dto/create-product.dto';
|
import { CreateProductDto } from './dto/create-product.dto';
|
||||||
import { UpdateUserDto } from './dto/update-product.dto';
|
import { UpdateProductDto } from './dto/update-product.dto';
|
||||||
import { Product } from './entities/inventory.entity';
|
import { Product } from './entities/inventory.entity';
|
||||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
|
|
||||||
@@ -53,8 +51,9 @@ export class InventoryService {
|
|||||||
id: products.id,
|
id: products.id,
|
||||||
title: products.title,
|
title: products.title,
|
||||||
description: products.description,
|
description: products.description,
|
||||||
valuePerUnit: products.valuePerUnit,
|
price: products.price,
|
||||||
urlImg: products.urlImg,
|
urlImg: products.urlImg,
|
||||||
|
stock: products.stock,
|
||||||
// price: products.price,
|
// price: products.price,
|
||||||
// quantity: products.quantity,
|
// quantity: products.quantity,
|
||||||
// isActive: products.isActive
|
// isActive: products.isActive
|
||||||
@@ -79,7 +78,7 @@ export class InventoryService {
|
|||||||
previousPage: page > 1 ? page - 1 : null,
|
previousPage: page > 1 ? page - 1 : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
return { data, meta };
|
return { data, meta };
|
||||||
}
|
}
|
||||||
@@ -90,8 +89,9 @@ export class InventoryService {
|
|||||||
id: products.id,
|
id: products.id,
|
||||||
title: products.title,
|
title: products.title,
|
||||||
description: products.description,
|
description: products.description,
|
||||||
valuePerUnit: products.valuePerUnit,
|
price: products.price,
|
||||||
urlImg: products.urlImg,
|
urlImg: products.urlImg,
|
||||||
|
stock: products.stock
|
||||||
})
|
})
|
||||||
.from(products)
|
.from(products)
|
||||||
// .leftJoin(usersRole, eq(usersRole.userId, users.id))
|
// .leftJoin(usersRole, eq(usersRole.userId, users.id))
|
||||||
@@ -124,8 +124,9 @@ export class InventoryService {
|
|||||||
.values({
|
.values({
|
||||||
title: createProductDto.title,
|
title: createProductDto.title,
|
||||||
description: createProductDto.description,
|
description: createProductDto.description,
|
||||||
valuePerUnit: createProductDto.valuePerUnit,
|
price: createProductDto.price,
|
||||||
urlImg: createProductDto.urlImg,
|
urlImg: createProductDto.urlImg,
|
||||||
|
stock: createProductDto.stock
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
37
apps/web/app/dashboard/inventario/page.tsx
Normal file
37
apps/web/app/dashboard/inventario/page.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import UsersAdminList from '@/feactures/inventory/components/inventory/product-inventory-list';
|
||||||
|
import { UsersHeader } from '@/feactures/inventory/components/inventory/users-header';
|
||||||
|
import UsersTableAction from '@/feactures/inventory/components/inventory/product-tables/users-table-action';
|
||||||
|
import { searchParamsCache, serialize } from '@/feactures/inventory/utils/searchparams';
|
||||||
|
import { SearchParams } from 'nuqs';
|
||||||
|
|
||||||
|
type pageProps = {
|
||||||
|
searchParams: Promise<SearchParams>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default async function SurveyAdminPage(props: pageProps) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
searchParamsCache.parse(searchParams);
|
||||||
|
const key = serialize({ ...searchParams });
|
||||||
|
|
||||||
|
const page = Number(searchParamsCache.get('page')) || 1;
|
||||||
|
const search = searchParamsCache.get('q');
|
||||||
|
const pageLimit = Number(searchParamsCache.get('limit')) || 10;
|
||||||
|
const type = searchParamsCache.get('type');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer scrollable={false}>
|
||||||
|
<div className="flex flex-1 flex-col space-y-4">
|
||||||
|
<UsersHeader />
|
||||||
|
<UsersTableAction />
|
||||||
|
<UsersAdminList
|
||||||
|
initialPage={page}
|
||||||
|
initialSearch={search}
|
||||||
|
initialLimit={pageLimit}
|
||||||
|
initialType={type}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
|
Blocks,
|
||||||
Check,
|
Check,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -40,6 +41,7 @@ export type Icon = LucideIcon;
|
|||||||
|
|
||||||
export const Icons = {
|
export const Icons = {
|
||||||
dashboard: LayoutDashboardIcon,
|
dashboard: LayoutDashboardIcon,
|
||||||
|
blocks: Blocks,
|
||||||
logo: Command,
|
logo: Command,
|
||||||
login: LogIn,
|
login: LogIn,
|
||||||
close: X,
|
close: X,
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ export const GeneralItems: NavItem[] = [
|
|||||||
isActive: false,
|
isActive: false,
|
||||||
items: [], // No child items
|
items: [], // No child items
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Inventario',
|
||||||
|
url: '/dashboard/inventario/',
|
||||||
|
icon: 'blocks',
|
||||||
|
shortcut: ['p', 'p'],
|
||||||
|
isActive: false,
|
||||||
|
items: [], // No child items
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
153
apps/web/feactures/inventory/actions/actions.ts
Normal file
153
apps/web/feactures/inventory/actions/actions.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
'use server';
|
||||||
|
import { safeFetchApi } from '@/lib/fetch.api';
|
||||||
|
import {
|
||||||
|
surveysApiResponseSchema,
|
||||||
|
CreateUser,
|
||||||
|
productMutate,
|
||||||
|
UpdateUser
|
||||||
|
} from '../schemas/inventory';
|
||||||
|
|
||||||
|
import { auth } from '@/lib/auth';
|
||||||
|
|
||||||
|
|
||||||
|
export const getProfileAction = async () => {
|
||||||
|
const session = await auth()
|
||||||
|
const id = session?.user?.id
|
||||||
|
|
||||||
|
const [error, response] = await safeFetchApi(
|
||||||
|
productMutate,
|
||||||
|
`/users/${id}`,
|
||||||
|
'GET'
|
||||||
|
);
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateProfileAction = async (payload: UpdateUser) => {
|
||||||
|
const { id, ...payloadWithoutId } = payload;
|
||||||
|
|
||||||
|
const [error, data] = await safeFetchApi(
|
||||||
|
productMutate,
|
||||||
|
`/users/profile/${id}`,
|
||||||
|
'PATCH',
|
||||||
|
payloadWithoutId,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(payload);
|
||||||
|
if (error) {
|
||||||
|
if (error.message === 'Email already exists') {
|
||||||
|
throw new Error('Ese correo ya está en uso');
|
||||||
|
}
|
||||||
|
// console.error('Error:', error);
|
||||||
|
throw new Error('Error al crear el usuario');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInventoryAction = async (params: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
page: (params.page || 1).toString(),
|
||||||
|
limit: (params.limit || 10).toString(),
|
||||||
|
...(params.search && { search: params.search }),
|
||||||
|
...(params.sortBy && { sortBy: params.sortBy }),
|
||||||
|
...(params.sortOrder && { sortOrder: params.sortOrder }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, response] = await safeFetchApi(
|
||||||
|
surveysApiResponseSchema,
|
||||||
|
`/inventory?${searchParams}`,
|
||||||
|
'GET',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// const transformedData = response?.data ? transformSurvey(response?.data) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response?.data || [],
|
||||||
|
meta: response?.meta || {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalCount: 0,
|
||||||
|
totalPages: 1,
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
nextPage: null,
|
||||||
|
previousPage: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUserAction = async (payload: CreateUser) => {
|
||||||
|
const { id, confirmPassword, ...payloadWithoutId } = payload;
|
||||||
|
|
||||||
|
const [error, data] = await safeFetchApi(
|
||||||
|
productMutate,
|
||||||
|
'/users',
|
||||||
|
'POST',
|
||||||
|
payloadWithoutId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error.message === 'Username already exists') {
|
||||||
|
throw new Error('Ese usuario ya existe');
|
||||||
|
}
|
||||||
|
if (error.message === 'Email already exists') {
|
||||||
|
throw new Error('Ese correo ya está en uso');
|
||||||
|
}
|
||||||
|
// console.error('Error:', error);
|
||||||
|
throw new Error('Error al crear el usuario');
|
||||||
|
}
|
||||||
|
|
||||||
|
return payloadWithoutId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserAction = async (payload: UpdateUser) => {
|
||||||
|
try {
|
||||||
|
const { id, ...payloadWithoutId } = payload;
|
||||||
|
|
||||||
|
const [error, data] = await safeFetchApi(
|
||||||
|
productMutate,
|
||||||
|
`/users/${id}`,
|
||||||
|
'PATCH',
|
||||||
|
payloadWithoutId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.log(data);
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
throw new Error(error?.message || 'Error al actualizar el usuario');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteUserAction = async (id: Number) => {
|
||||||
|
const [error] = await safeFetchApi(
|
||||||
|
productMutate,
|
||||||
|
`/users/${id}`,
|
||||||
|
'DELETE'
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
|
||||||
|
// if (error) throw new Error(error.message || 'Error al eliminar el usuario')
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@repo/shadcn/form';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useCreateUser } from "../../hooks/use-mutation-users";
|
||||||
|
import { CreateUser, createUser } from '../../schemas/inventory';
|
||||||
|
|
||||||
|
const ROLES = {
|
||||||
|
// 1: 'Superadmin',
|
||||||
|
2: 'Administrador',
|
||||||
|
3: 'autoridad',
|
||||||
|
4: 'Gerente',
|
||||||
|
5: 'Usuario',
|
||||||
|
6: 'Productor',
|
||||||
|
7: 'Organización'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateUserFormProps {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
defaultValues?: Partial<CreateUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateUserForm({
|
||||||
|
onSuccess,
|
||||||
|
onCancel,
|
||||||
|
defaultValues,
|
||||||
|
}: CreateUserFormProps) {
|
||||||
|
const {
|
||||||
|
mutate: saveAccountingAccounts,
|
||||||
|
isPending: isSaving,
|
||||||
|
isError,
|
||||||
|
} = useCreateUser();
|
||||||
|
|
||||||
|
// const { data: AccoutingAccounts } = useSurveyMutation();
|
||||||
|
|
||||||
|
const defaultformValues = {
|
||||||
|
username: defaultValues?.username || '',
|
||||||
|
fullname: defaultValues?.fullname || '',
|
||||||
|
email: defaultValues?.email || '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
id: defaultValues?.id,
|
||||||
|
phone: defaultValues?.phone || '',
|
||||||
|
role: defaultValues?.role,
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = useForm<CreateUser>({
|
||||||
|
resolver: zodResolver(createUser),
|
||||||
|
defaultValues: defaultformValues,
|
||||||
|
mode: 'onChange', // Enable real-time validation
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: CreateUser) => {
|
||||||
|
|
||||||
|
const formData = data
|
||||||
|
|
||||||
|
saveAccountingAccounts(formData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
form.reset();
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
form.setError('root', {
|
||||||
|
type: 'manual',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{form.formState.errors.root && (
|
||||||
|
<div className="text-destructive text-sm">
|
||||||
|
{form.formState.errors.root.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Usuario</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fullname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre completo</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Correo</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="phone"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Teléfono</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} value={field.value?.toString() ?? ''}/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Contraseña</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" {...field}/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='confirmPassword'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Confirmar Contraseña</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" {...field}/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="role"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Rol</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => field.onChange(Number(value))}
|
||||||
|
defaultValue={String(field.value)}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Selecciona un rol" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent className="w-full min-w-[200px]">
|
||||||
|
{Object.entries(ROLES).map(([value, label]) => (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button variant="outline" type="button" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSaving}>
|
||||||
|
{isSaving ? 'Guardando...' : 'Guardar'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { DataTable } from '@repo/shadcn/table/data-table';
|
||||||
|
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
||||||
|
import { columns } from './product-tables/columns';
|
||||||
|
import { useProductQuery } from '../../hooks/use-query-users';
|
||||||
|
|
||||||
|
interface SurveysAdminListProps {
|
||||||
|
initialPage: number;
|
||||||
|
initialSearch?: string | null;
|
||||||
|
initialLimit: number;
|
||||||
|
initialType?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UsersAdminList({
|
||||||
|
initialPage,
|
||||||
|
initialSearch,
|
||||||
|
initialLimit,
|
||||||
|
initialType,
|
||||||
|
}: SurveysAdminListProps) {
|
||||||
|
const filters = {
|
||||||
|
page: initialPage,
|
||||||
|
limit: initialLimit,
|
||||||
|
...(initialSearch && { search: initialSearch }),
|
||||||
|
...(initialType && { type: initialType }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const {data, isLoading} = useProductQuery(filters)
|
||||||
|
|
||||||
|
|
||||||
|
// const {data, isLoading} = useUsersQuery(filters)
|
||||||
|
|
||||||
|
console.log(data?.data);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <DataTableSkeleton columnCount={6} rowCount={initialLimit} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={data?.data || []}
|
||||||
|
totalItems={data?.meta.totalCount || 0}
|
||||||
|
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { AlertModal } from '@/components/modal/alert-modal';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@repo/shadcn/tooltip';
|
||||||
|
import { Edit, Trash, User } from 'lucide-react';
|
||||||
|
import { InventoryTable } from '@/feactures/inventory/schemas/inventory';
|
||||||
|
import { useDeleteUser } from '@/feactures/users/hooks/use-mutation-users';
|
||||||
|
import { AccountPlanModal } from '../user-modal';
|
||||||
|
|
||||||
|
interface CellActionProps {
|
||||||
|
data: InventoryTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [edit, setEdit] = useState(false);
|
||||||
|
const { mutate: deleteUser } = useDeleteUser();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onConfirm = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
deleteUser(data.id!);
|
||||||
|
setOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
loading={loading}
|
||||||
|
title="¿Estás seguro que desea deshabilitar este usuario?"
|
||||||
|
description="Esta acción no se puede deshacer."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AccountPlanModal open={edit} onOpenChange={setEdit} defaultValues={data}/>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setEdit(true)}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Editar</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Deshabilitar</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Badge } from "@repo/shadcn/badge";
|
||||||
|
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { CellAction } from './cell-action';
|
||||||
|
import { InventoryTable } from '../../../schemas/inventory';
|
||||||
|
|
||||||
|
export const columns: ColumnDef<InventoryTable>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'urlImg',
|
||||||
|
header: 'img',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.getValue("urlImg") as string | undefined;
|
||||||
|
return (
|
||||||
|
<img src={`http://localhost:3000/${status}`} alt="Image" width={64} height={64} className="rounded"/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'title',
|
||||||
|
header: 'Producto',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "description",
|
||||||
|
header: "Descripcion",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'price',
|
||||||
|
header: 'Precio',
|
||||||
|
cell: ({ row }) => `${row.original.price}$`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'stock',
|
||||||
|
header: 'Stock',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Acciones',
|
||||||
|
cell: ({ row }) => <CellAction data={row.original} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { PUBLISHED_TYPES } from '@/feactures/surveys/schemas/surveys-options';
|
||||||
|
import { searchParams } from '@repo/shadcn/lib/searchparams';
|
||||||
|
import { useQueryState } from 'nuqs';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
export const TYPE_OPTIONS = Object.entries(PUBLISHED_TYPES).map(
|
||||||
|
([value, label]) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export function useSurveyTableFilters() {
|
||||||
|
const [searchQuery, setSearchQuery] = useQueryState(
|
||||||
|
'q',
|
||||||
|
searchParams.q
|
||||||
|
.withOptions({
|
||||||
|
shallow: false,
|
||||||
|
throttleMs: 500, // Add 500ms delay
|
||||||
|
// Removed dedupingInterval as it's not a valid option
|
||||||
|
})
|
||||||
|
.withDefault(''),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [typeFilter, setTypeFilter] = useQueryState(
|
||||||
|
'published',
|
||||||
|
searchParams.q.withOptions({ shallow: false }).withDefault(''),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [page, setPage] = useQueryState(
|
||||||
|
'page',
|
||||||
|
searchParams.page.withDefault(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetFilters = useCallback(() => {
|
||||||
|
setSearchQuery(null);
|
||||||
|
setTypeFilter(null);
|
||||||
|
setPage(1);
|
||||||
|
}, [setSearchQuery, setPage]);
|
||||||
|
|
||||||
|
const isAnyFilterActive = useMemo(() => {
|
||||||
|
return !!searchQuery || !!typeFilter;
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
page,
|
||||||
|
setPage,
|
||||||
|
resetFilters,
|
||||||
|
isAnyFilterActive,
|
||||||
|
typeFilter,
|
||||||
|
setTypeFilter
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { DataTableFilterBox } from '@repo/shadcn/table/data-table-filter-box';
|
||||||
|
import { DataTableSearch } from '@repo/shadcn/table/data-table-search';
|
||||||
|
import {
|
||||||
|
TYPE_OPTIONS,
|
||||||
|
useSurveyTableFilters,
|
||||||
|
} from './use-survey-table-filters';
|
||||||
|
|
||||||
|
export default function UserTableAction() {
|
||||||
|
const {
|
||||||
|
typeFilter,
|
||||||
|
searchQuery,
|
||||||
|
setPage,
|
||||||
|
setTypeFilter,
|
||||||
|
setSearchQuery,
|
||||||
|
} = useSurveyTableFilters();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center gap-4 pt-2">
|
||||||
|
<DataTableSearch
|
||||||
|
searchKey={searchQuery}
|
||||||
|
searchQuery={searchQuery || ''}
|
||||||
|
setSearchQuery={setSearchQuery}
|
||||||
|
setPage={setPage}
|
||||||
|
/>
|
||||||
|
{/* <DataTableFilterBox
|
||||||
|
filterKey="type"
|
||||||
|
title="Estado"
|
||||||
|
options={TYPE_OPTIONS}
|
||||||
|
setFilterValue={setTypeFilter}
|
||||||
|
filterValue={typeFilter}
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@repo/shadcn/form';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useUpdateUser } from "@/feactures/users/hooks/use-mutation-users";
|
||||||
|
import { UpdateUser, updateUser } from '@/feactures/users/schemas/users';
|
||||||
|
|
||||||
|
const ROLES = {
|
||||||
|
// 1: 'Superadmin',
|
||||||
|
2: 'Administrador',
|
||||||
|
3: 'autoridad',
|
||||||
|
4: 'Gerente',
|
||||||
|
5: 'Usuario',
|
||||||
|
6: 'Productor',
|
||||||
|
7: 'Organización'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserFormProps {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
defaultValues?: Partial<UpdateUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpdateUserForm({
|
||||||
|
onSuccess,
|
||||||
|
onCancel,
|
||||||
|
defaultValues,
|
||||||
|
}: UserFormProps) {
|
||||||
|
const {
|
||||||
|
mutate: saveAccountingAccounts,
|
||||||
|
isPending: isSaving,
|
||||||
|
isError,
|
||||||
|
} = useUpdateUser();
|
||||||
|
|
||||||
|
const defaultformValues = {
|
||||||
|
username: defaultValues?.username || '',
|
||||||
|
fullname: defaultValues?.fullname || '',
|
||||||
|
email: defaultValues?.email || '',
|
||||||
|
password: '',
|
||||||
|
id: defaultValues?.id,
|
||||||
|
phone: defaultValues?.phone || '',
|
||||||
|
role: undefined,
|
||||||
|
isActive: defaultValues?.isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(defaultValues);
|
||||||
|
|
||||||
|
const form = useForm<UpdateUser>({
|
||||||
|
resolver: zodResolver(updateUser),
|
||||||
|
defaultValues: defaultformValues,
|
||||||
|
mode: 'onChange', // Enable real-time validation
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: UpdateUser) => {
|
||||||
|
|
||||||
|
const formData = data
|
||||||
|
|
||||||
|
saveAccountingAccounts(formData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
form.reset();
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
form.setError('root', {
|
||||||
|
type: 'manual',
|
||||||
|
message: 'Error al guardar la cuenta contable',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{form.formState.errors.root && (
|
||||||
|
<div className="text-destructive text-sm">
|
||||||
|
{form.formState.errors.root.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Usuario</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fullname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre completo</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Correo</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="phone"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Teléfono</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} value={field.value?.toString() ?? ''}/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='password'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nueva Contraseña</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" {...field}/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="role"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Rol</FormLabel>
|
||||||
|
<Select onValueChange={(value) => field.onChange(Number(value))}
|
||||||
|
// defaultValue={String(field.value)}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Selecciona un rol" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent className="w-full min-w-[200px]">
|
||||||
|
{Object.entries(ROLES).map(([value, label]) => (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isActive"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Estatus</FormLabel>
|
||||||
|
<Select defaultValue={String(field.value)} onValueChange={(value) => field.onChange(Boolean(value))}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Seleccione un estatus" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="true">Activo</SelectItem>
|
||||||
|
<SelectItem value="false">Inactivo</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button variant="outline" type="button" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSaving}>
|
||||||
|
{isSaving ? 'Guardando...' : 'Guardar'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@repo/shadcn/dialog';
|
||||||
|
import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema';
|
||||||
|
import { CreateUserForm } from './create-user-form';
|
||||||
|
import { UpdateUserForm } from './update-user-form';
|
||||||
|
|
||||||
|
interface AccountPlanModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
defaultValues?: Partial<AccountPlan>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountPlanModal({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
defaultValues,
|
||||||
|
}: AccountPlanModalProps) {
|
||||||
|
const handleSuccess = () => {
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-[600px] z-50 backdrop-blur-lg bg-background/80">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{defaultValues?.id
|
||||||
|
? 'Actualizar usuario'
|
||||||
|
: 'Crear usuario'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Complete los campos para {defaultValues?.id ? 'actualizar' : 'crear'} un usuario
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{defaultValues?.id ? (
|
||||||
|
<UpdateUserForm
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
/>
|
||||||
|
): (
|
||||||
|
<CreateUserForm
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
'use client';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import { Heading } from '@repo/shadcn/heading';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { AccountPlanModal } from './user-modal';
|
||||||
|
|
||||||
|
export function UsersHeader() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
// const router = useRouter();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<Heading
|
||||||
|
title="Administración del inventario"
|
||||||
|
description="Gestione aquí los productos que usted registre en la plataforma"
|
||||||
|
/>
|
||||||
|
<Button onClick={() => setOpen(true)} size="sm">
|
||||||
|
<Plus className="mr-2 h-4 w-4" /> Agregar Usuario
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AccountPlanModal open={open} onOpenChange={setOpen} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
apps/web/feactures/inventory/components/modal-profile.tsx
Normal file
57
apps/web/feactures/inventory/components/modal-profile.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@repo/shadcn/dialog';
|
||||||
|
import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema';
|
||||||
|
import { ModalForm } from './update-user-form';
|
||||||
|
|
||||||
|
interface AccountPlanModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
defaultValues?: Partial<AccountPlan>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountPlanModal({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
defaultValues,
|
||||||
|
}: AccountPlanModalProps) {
|
||||||
|
const handleSuccess = () => {
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-[600px] z-50 backdrop-blur-lg bg-background/80">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Actualizar Perfil</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Complete los campos para actualizar sus datos.<br/>Los campos vacios no seran actualizados.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<ModalForm
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
apps/web/feactures/inventory/components/selectList.tsx
Normal file
85
apps/web/feactures/inventory/components/selectList.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// 'use client';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@repo/shadcn/form';
|
||||||
|
import { UpdateUser, updateUser } from '@/feactures/users/schemas/users';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
interface SelectListProps {
|
||||||
|
label: string
|
||||||
|
// values:
|
||||||
|
values: Array<object>
|
||||||
|
form: any
|
||||||
|
name: string
|
||||||
|
handleChange: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectList({ label, values, form, name, handleChange }: SelectListProps) {
|
||||||
|
// const { label, values, form, name } = props;
|
||||||
|
// handleChange
|
||||||
|
|
||||||
|
// const defaultformValues = {
|
||||||
|
// username: '',
|
||||||
|
// fullname: '',
|
||||||
|
// email: '',
|
||||||
|
// password: '',
|
||||||
|
// id: 0,
|
||||||
|
// phone: '',
|
||||||
|
// role: undefined,
|
||||||
|
// isActive: false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const form = useForm<UpdateUser>({
|
||||||
|
// resolver: zodResolver(updateUser),
|
||||||
|
// defaultValues: defaultformValues,
|
||||||
|
// mode: 'onChange', // Enable real-time validation
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
return <FormField
|
||||||
|
control={form.control}
|
||||||
|
name={name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>{label}</FormLabel>
|
||||||
|
<Select onValueChange={handleChange}
|
||||||
|
// defaultValue={String(field.value)}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Selecciona una opción" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent className="w-full min-w-[200px]">
|
||||||
|
{values.map((item: any) => (
|
||||||
|
<SelectItem key={item.id} value={item.id}>
|
||||||
|
{item.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
{/* <SelectItem key={0} value="0">Hola1</SelectItem>
|
||||||
|
<SelectItem key={1} value="1">Hola2</SelectItem> */}
|
||||||
|
{/* {Object.entries(values).map(([id, label]) => (
|
||||||
|
<SelectItem key={id} value={id}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))} */}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
28
apps/web/feactures/inventory/components/survey.tsx
Normal file
28
apps/web/feactures/inventory/components/survey.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { SurveyResponse } from '@/feactures/surveys/components/survey-response';
|
||||||
|
import { useSurveysByIdQuery } from '@/feactures/surveys/hooks/use-query-surveys';
|
||||||
|
|
||||||
|
import { notFound, useParams } from 'next/navigation';
|
||||||
|
|
||||||
|
|
||||||
|
export default function SurveyPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const surveyId = params?.id as string | undefined;
|
||||||
|
|
||||||
|
|
||||||
|
if (!surveyId || surveyId === '') {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: survey, isLoading } = useSurveysByIdQuery(Number(surveyId));
|
||||||
|
console.log('🎯 useSurveysByIdQuery ejecutado, data:', survey, 'isLoading:', isLoading);
|
||||||
|
|
||||||
|
if (!survey?.data || !survey?.data.published) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SurveyResponse survey={survey?.data} />
|
||||||
|
);
|
||||||
|
}
|
||||||
268
apps/web/feactures/inventory/components/update-user-form.tsx
Normal file
268
apps/web/feactures/inventory/components/update-user-form.tsx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@repo/shadcn/form';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
// import {
|
||||||
|
// Select,
|
||||||
|
// SelectContent,
|
||||||
|
// SelectItem,
|
||||||
|
// SelectTrigger,
|
||||||
|
// SelectValue,
|
||||||
|
// } from '@repo/shadcn/select';
|
||||||
|
import { SelectSearchable } from '@repo/shadcn/select-searchable'
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useUpdateProfile } from "@/feactures/users/hooks/use-mutation-users";
|
||||||
|
import { UpdateUser, updateUser } from '@/feactures/users/schemas/users';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location';
|
||||||
|
|
||||||
|
interface UserFormProps {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
defaultValues?: Partial<UpdateUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalForm({
|
||||||
|
onSuccess,
|
||||||
|
onCancel,
|
||||||
|
defaultValues,
|
||||||
|
}: UserFormProps) {
|
||||||
|
const {
|
||||||
|
mutate: saveAccountingAccounts,
|
||||||
|
isPending: isSaving,
|
||||||
|
isError,
|
||||||
|
} = useUpdateProfile();
|
||||||
|
|
||||||
|
const [state, setState] = React.useState(0);
|
||||||
|
const [municipality, setMunicipality] = React.useState(0);
|
||||||
|
const [parish, setParish] = React.useState(0);
|
||||||
|
|
||||||
|
const [disabledMunicipality, setDisabledMunicipality] = React.useState(true);
|
||||||
|
const [disabledParish, setDisabledParish] = React.useState(true);
|
||||||
|
|
||||||
|
const { data : dataState } = useStateQuery()
|
||||||
|
const { data : dataMunicipality } = useMunicipalityQuery(state)
|
||||||
|
const { data : dataParish } = useParishQuery(municipality)
|
||||||
|
|
||||||
|
const stateOptions = dataState?.data || [{id:0,name:'Sin estados'}]
|
||||||
|
|
||||||
|
const municipalityOptions = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
|
||||||
|
? dataMunicipality.data
|
||||||
|
: [{id:0,stateId:0,name:'Sin Municipios'}]
|
||||||
|
// const parishOptions = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}]
|
||||||
|
const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0
|
||||||
|
? dataParish.data
|
||||||
|
: [{id:0,stateId:0,name:'Sin Parroquias'}]
|
||||||
|
|
||||||
|
|
||||||
|
const defaultformValues = {
|
||||||
|
username: defaultValues?.username || '',
|
||||||
|
fullname: defaultValues?.fullname || '',
|
||||||
|
email: defaultValues?.email || '',
|
||||||
|
password: '',
|
||||||
|
id: defaultValues?.id,
|
||||||
|
phone: defaultValues?.phone || '',
|
||||||
|
role: undefined,
|
||||||
|
isActive: defaultValues?.isActive,
|
||||||
|
state: defaultValues?.state,
|
||||||
|
municipality: defaultValues?.municipality,
|
||||||
|
parish: defaultValues?.parish
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// console.log(defaultValues);
|
||||||
|
|
||||||
|
const form = useForm<UpdateUser>({
|
||||||
|
resolver: zodResolver(updateUser),
|
||||||
|
defaultValues: defaultformValues,
|
||||||
|
mode: 'onChange', // Enable real-time validation
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: UpdateUser) => {
|
||||||
|
|
||||||
|
const formData = data
|
||||||
|
|
||||||
|
saveAccountingAccounts(formData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
form.reset();
|
||||||
|
onSuccess?.();
|
||||||
|
toast.success('Actualizado exitosamente!');
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
form.setError('root', {
|
||||||
|
type: 'manual',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
// toast.error(e.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{form.formState.errors.root && (
|
||||||
|
<div className="text-destructive text-sm">
|
||||||
|
{form.formState.errors.root.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fullname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre completo</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Correo</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="phone"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Teléfono</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} value={field.value?.toString() ?? ''}/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="state"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Estado</FormLabel>
|
||||||
|
|
||||||
|
<SelectSearchable
|
||||||
|
options={
|
||||||
|
stateOptions?.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
onValueChange={(value : any) =>
|
||||||
|
{field.onChange(Number(value)); setState(value); setDisabledMunicipality(false); setDisabledParish(true)}
|
||||||
|
}
|
||||||
|
placeholder="Selecciona un estado"
|
||||||
|
defaultValue={field.value?.toString()}
|
||||||
|
// disabled={readOnly}
|
||||||
|
/>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="municipality"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Municipio</FormLabel>
|
||||||
|
|
||||||
|
<SelectSearchable
|
||||||
|
options={
|
||||||
|
municipalityOptions?.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
onValueChange={(value : any) =>
|
||||||
|
{field.onChange(Number(value)); setMunicipality(value); setDisabledParish(false)}
|
||||||
|
}
|
||||||
|
placeholder="Selecciona un Municipio"
|
||||||
|
defaultValue={field.value?.toString()}
|
||||||
|
disabled={disabledMunicipality}
|
||||||
|
/>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="parish"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Parroquia</FormLabel>
|
||||||
|
|
||||||
|
<SelectSearchable
|
||||||
|
options={
|
||||||
|
parishOptions?.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
onValueChange={(value : any) =>
|
||||||
|
field.onChange(Number(value))
|
||||||
|
}
|
||||||
|
placeholder="Selecciona una Parroquia"
|
||||||
|
defaultValue={field.value?.toString()}
|
||||||
|
disabled={disabledParish}
|
||||||
|
/>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='password'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nueva Contraseña</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" {...field}/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button variant="outline" type="button" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSaving}>
|
||||||
|
{isSaving ? 'Guardando...' : 'Guardar'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
apps/web/feactures/inventory/components/user-profile.tsx
Normal file
75
apps/web/feactures/inventory/components/user-profile.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
'use client';
|
||||||
|
import { useUserByProfile } from '@/feactures/users/hooks/use-query-users';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import { Edit, Edit2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { AccountPlanModal } from './modal-profile';
|
||||||
|
|
||||||
|
export function Profile() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data } = useUserByProfile();
|
||||||
|
|
||||||
|
// console.log("🎯 data:", data);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setOpen(true)} size="sm">
|
||||||
|
<Edit2 className="mr-2 h-4 w-4" /> Editar Perfil
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<AccountPlanModal open={open} onOpenChange={setOpen} defaultValues={data?.data}/>
|
||||||
|
|
||||||
|
<h2 className='mt-3 mb-1'>Datos del usuario</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<section className='border bg-muted p-2 rounded-md'>
|
||||||
|
<p className='font-bold text-lg'>Usuario:</p>
|
||||||
|
<p>{data?.data.username || 'Sin Nombre de Usuario'}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className='border bg-muted p-2 rounded-md'>
|
||||||
|
<p className='font-bold text-lg'>Rol:</p>
|
||||||
|
<p>{data?.data.role || 'Sin Rol'}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className='mt-3 mb-1'>Información personal</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<section className='border bg-muted p-2 rounded-md'>
|
||||||
|
<p className='font-bold text-lg'>Nombre completo:</p>
|
||||||
|
<p>{data?.data.fullname || 'Sin nombre y apellido'}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className='border bg-muted p-2 rounded-md'>
|
||||||
|
<p className='font-bold text-lg'>Correo:</p>
|
||||||
|
<p>{data?.data.email || 'Sin correo'}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className='border bg-muted p-2 rounded-md'>
|
||||||
|
<p className='font-bold text-lg'>Teléfono:</p>
|
||||||
|
<p>{data?.data.phone || 'Sin teléfono'}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className='mt-3 mb-1'>Información de ubicación</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<section className='border bg-muted p-2 rounded-md'>
|
||||||
|
<p className='font-bold text-lg'>Estado:</p>
|
||||||
|
<p>{data?.data.state || 'Sin Estado'}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className='border bg-muted p-2 rounded-md'>
|
||||||
|
<p className='font-bold text-lg'>Municipio:</p>
|
||||||
|
<p>{data?.data.municipality || 'Sin Municipio'}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className='border bg-muted p-2 rounded-md'>
|
||||||
|
<p className='font-bold text-lg'>Parroquia:</p>
|
||||||
|
<p>{data?.data.parish || 'Sin Parroquia'}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
45
apps/web/feactures/inventory/hooks/use-mutation-users.ts
Normal file
45
apps/web/feactures/inventory/hooks/use-mutation-users.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { CreateUser, UpdateUser } from "../schemas/inventory";
|
||||||
|
import { updateUserAction, createUserAction, deleteUserAction, updateProfileAction } from "../actions/actions";
|
||||||
|
|
||||||
|
// Create mutation
|
||||||
|
export function useCreateUser() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (data: CreateUser) => createUserAction(data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
||||||
|
// onError: (e) => console.error('Error:', e),
|
||||||
|
})
|
||||||
|
return mutation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update mutation
|
||||||
|
export function useUpdateUser() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (data: UpdateUser) => updateUserAction(data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
||||||
|
onError: (e) => console.error('Error:', e)
|
||||||
|
})
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateProfile() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (data: UpdateUser) => updateProfileAction(data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
||||||
|
// onError: (e) => console.error('Error:', e)
|
||||||
|
})
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete mutation
|
||||||
|
export function useDeleteUser() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => deleteUserAction(id),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
||||||
|
onError: (e) => console.error('Error:', e)
|
||||||
|
})
|
||||||
|
}
|
||||||
12
apps/web/feactures/inventory/hooks/use-query-surveys.ts
Normal file
12
apps/web/feactures/inventory/hooks/use-query-surveys.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'use client'
|
||||||
|
import { useSafeQuery } from "@/hooks/use-safe-query";
|
||||||
|
import { getUsersAction,getProfileAction} from "../actions/actions";
|
||||||
|
|
||||||
|
// Hook for users
|
||||||
|
export function useUsersQuery(params = {}) {
|
||||||
|
return useSafeQuery(['users',params], () => getUsersAction(params))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUserByProfile() {
|
||||||
|
return useSafeQuery(['users'], () => getProfileAction())
|
||||||
|
}
|
||||||
8
apps/web/feactures/inventory/hooks/use-query-users.ts
Normal file
8
apps/web/feactures/inventory/hooks/use-query-users.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use client'
|
||||||
|
import { useSafeQuery } from "@/hooks/use-safe-query";
|
||||||
|
import { getInventoryAction} from "../actions/actions";
|
||||||
|
|
||||||
|
// Hook for users
|
||||||
|
export function useProductQuery(params = {}) {
|
||||||
|
return useSafeQuery(['product',params], () => getInventoryAction(params))
|
||||||
|
}
|
||||||
19
apps/web/feactures/inventory/schemas/account-plan-options.ts
Normal file
19
apps/web/feactures/inventory/schemas/account-plan-options.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const ACCOUNT_TYPES = {
|
||||||
|
activo: 'Activo',
|
||||||
|
pasivo: 'Pasivo',
|
||||||
|
patrimonio: 'Patrimonio',
|
||||||
|
ingreso: 'Ingreso',
|
||||||
|
gasto: 'Gasto',
|
||||||
|
costo: 'Costo',
|
||||||
|
cuenta_orden: 'Cuenta de Orden',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ACCOUNT_LEVELS = {
|
||||||
|
1: 'Nivel 1 - Cuenta Principal',
|
||||||
|
2: 'Nivel 2 - Subcuenta',
|
||||||
|
3: 'Nivel 3 - Cuenta Detallada',
|
||||||
|
4: 'Nivel 4 - Cuenta Auxiliar',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AccountType = keyof typeof ACCOUNT_TYPES;
|
||||||
|
export type AccountLevel = keyof typeof ACCOUNT_LEVELS;
|
||||||
83
apps/web/feactures/inventory/schemas/account-plan.schema.ts
Normal file
83
apps/web/feactures/inventory/schemas/account-plan.schema.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const accountPlanSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.number().optional(),
|
||||||
|
savingBankId: z.number(),
|
||||||
|
code: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'El código es requerido')
|
||||||
|
.max(50, 'El código no puede tener más de 50 caracteres')
|
||||||
|
.regex(/^[\d.]+$/, 'El código debe contener solo números y puntos'),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'El nombre es requerido')
|
||||||
|
.max(100, 'El nombre no puede tener más de 100 caracteres'),
|
||||||
|
type: z.enum(
|
||||||
|
[
|
||||||
|
'activo',
|
||||||
|
'pasivo',
|
||||||
|
'patrimonio',
|
||||||
|
'ingreso',
|
||||||
|
'gasto',
|
||||||
|
'costo',
|
||||||
|
'cuenta_orden',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
required_error: 'El tipo de cuenta es requerido',
|
||||||
|
invalid_type_error: 'Tipo de cuenta inválido',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
description: z.string().optional().nullable(),
|
||||||
|
level: z
|
||||||
|
.number()
|
||||||
|
.min(1, 'El nivel debe ser mayor a 0')
|
||||||
|
.max(4, 'El nivel no puede ser mayor a 4'),
|
||||||
|
parent_account_id: z.number().nullable(),
|
||||||
|
created_at: z.string().optional(),
|
||||||
|
updated_at: z.string().optional(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.level > 1 && !data.parent_account_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Las cuentas de nivel superior a 1 requieren una cuenta padre',
|
||||||
|
path: ['parent_account_id'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type AccountPlan = z.infer<typeof accountPlanSchema>;
|
||||||
|
|
||||||
|
// Response schemas for the API
|
||||||
|
export const accountPlanResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: accountPlanSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const accountPlanDeleteResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const accountPlanListResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: z.array(accountPlanSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const accountPlanPaginationResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: z.array(accountPlanSchema),
|
||||||
|
meta: z.object({
|
||||||
|
page: z.number(),
|
||||||
|
limit: z.number(),
|
||||||
|
totalCount: z.number(),
|
||||||
|
totalPages: z.number(),
|
||||||
|
hasNextPage: z.boolean(),
|
||||||
|
hasPreviousPage: z.boolean(),
|
||||||
|
nextPage: z.number().nullable(),
|
||||||
|
previousPage: z.number().nullable(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
69
apps/web/feactures/inventory/schemas/inventory.ts
Normal file
69
apps/web/feactures/inventory/schemas/inventory.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { title } from 'process';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export type InventoryTable = z.infer<typeof product>;
|
||||||
|
export type CreateUser = z.infer<typeof createUser>;
|
||||||
|
export type UpdateUser = z.infer<typeof updateUser>;
|
||||||
|
|
||||||
|
export const product = z.object({
|
||||||
|
id: z.number().optional(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
// price: z.number(),
|
||||||
|
// quantity: z.number(),
|
||||||
|
// category: z.string(),
|
||||||
|
// image: z.string().optional(),
|
||||||
|
stock: z.number(),
|
||||||
|
price: z.string(),
|
||||||
|
urlImg: z.string(),
|
||||||
|
userId: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createUser = z.object({
|
||||||
|
id: z.number().optional(),
|
||||||
|
username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }),
|
||||||
|
password: z.string().min(8, { message: "Debe de tener 8 o más caracteres" }),
|
||||||
|
email: z.string().email({ message: "Correo no válido" }),
|
||||||
|
fullname: z.string(),
|
||||||
|
phone: z.string(),
|
||||||
|
confirmPassword: z.string(),
|
||||||
|
role: z.number()
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: 'La contraseña no coincide',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateUser = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }).or(z.literal('')),
|
||||||
|
password: z.string().min(6, { message: "Debe de tener 6 o más caracteres" }).or(z.literal('')),
|
||||||
|
email: z.string().email({ message: "Correo no válido" }).or(z.literal('')),
|
||||||
|
fullname: z.string().optional(),
|
||||||
|
phone: z.string().optional(),
|
||||||
|
role: z.number().optional(),
|
||||||
|
isActive: z.boolean().optional(),
|
||||||
|
state: z.number().optional().nullable(),
|
||||||
|
municipality: z.number().optional().nullable(),
|
||||||
|
parish: z.number().optional().nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const surveysApiResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: z.array(product),
|
||||||
|
meta: z.object({
|
||||||
|
page: z.number(),
|
||||||
|
limit: z.number(),
|
||||||
|
totalCount: z.number(),
|
||||||
|
totalPages: z.number(),
|
||||||
|
hasNextPage: z.boolean(),
|
||||||
|
hasPreviousPage: z.boolean(),
|
||||||
|
nextPage: z.number().nullable(),
|
||||||
|
previousPage: z.number().nullable(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const productMutate = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: product,
|
||||||
|
})
|
||||||
6
apps/web/feactures/inventory/schemas/surveys-options.ts
Normal file
6
apps/web/feactures/inventory/schemas/surveys-options.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const PUBLISHED_TYPES = {
|
||||||
|
published: 'Publicada',
|
||||||
|
draft: 'Borrador',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PublishedType = keyof typeof PUBLISHED_TYPES;
|
||||||
11
apps/web/feactures/inventory/utils/date-utils.ts
Normal file
11
apps/web/feactures/inventory/utils/date-utils.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export function formatDate(date: Date): string {
|
||||||
|
return new Intl.DateTimeFormat('es-ES', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
16
apps/web/feactures/inventory/utils/searchparams.ts
Normal file
16
apps/web/feactures/inventory/utils/searchparams.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {
|
||||||
|
createSearchParamsCache,
|
||||||
|
createSerializer,
|
||||||
|
parseAsInteger,
|
||||||
|
parseAsString,
|
||||||
|
} from 'nuqs/server';
|
||||||
|
|
||||||
|
export const searchParams = {
|
||||||
|
page: parseAsInteger.withDefault(1),
|
||||||
|
limit: parseAsInteger.withDefault(10),
|
||||||
|
q: parseAsString,
|
||||||
|
type: parseAsString,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchParamsCache = createSearchParamsCache(searchParams);
|
||||||
|
export const serialize = createSerializer(searchParams);
|
||||||
BIN
apps/web/public/apple.avif
Normal file
BIN
apps/web/public/apple.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
Reference in New Issue
Block a user