+
+
{icon}
-
{label}
+
{label}
-
+
$ {value.toLocaleString('es-AR', { minimumFractionDigits: 2 })}
diff --git a/frontend/counter-panel/src/layouts/CounterLayout.tsx b/frontend/counter-panel/src/layouts/CounterLayout.tsx
index 9aeb7bc..ffa2ff0 100644
--- a/frontend/counter-panel/src/layouts/CounterLayout.tsx
+++ b/frontend/counter-panel/src/layouts/CounterLayout.tsx
@@ -70,7 +70,7 @@ export default function CounterLayout() {
const menuItems = [
{ path: '/dashboard', label: 'Panel Principal', icon: LayoutDashboard, shortcut: 'F1' },
- { path: '/nuevo-aviso', label: 'Nuevo Aviso', icon: PlusCircle, shortcut: 'F2' },
+ { path: '/nuevo-aviso', label: 'Operar Caja', icon: PlusCircle, shortcut: 'F2' },
{ path: '/caja', label: 'Caja Diaria', icon: Banknote, shortcut: 'F4' },
{ path: '/historial', label: 'Consultas', icon: ClipboardList, shortcut: 'F8' },
{ path: '/analitica', label: 'Analítica', icon: TrendingUp, shortcut: 'F6' },
diff --git a/frontend/counter-panel/src/pages/FastEntryPage.tsx b/frontend/counter-panel/src/pages/FastEntryPage.tsx
index ef1e00d..e9fbf1b 100644
--- a/frontend/counter-panel/src/pages/FastEntryPage.tsx
+++ b/frontend/counter-panel/src/pages/FastEntryPage.tsx
@@ -7,7 +7,13 @@ import {
AlignLeft, AlignCenter, AlignRight, AlignJustify,
Type, Search, ChevronDown, Bold, Square as FrameIcon,
ArrowUpRight,
- RefreshCw
+ RefreshCw,
+ Calendar,
+ Image as ImageIcon,
+ X,
+ UploadCloud,
+ MessageSquare,
+ Star
} from 'lucide-react';
import clsx from 'clsx';
import PaymentModal, { type Payment } from '../components/PaymentModal';
@@ -48,8 +54,13 @@ export default function FastEntryPage() {
const catWrapperRef = useRef
(null);
const [formData, setFormData] = useState({
- categoryId: '', operationId: '', text: '', days: 3, clientName: '', clientDni: '',
+ categoryId: '', operationId: '', text: '', title: '', price: '', days: 3, clientName: '', clientDni: '',
+ startDate: new Date(Date.now() + 86400000).toISOString().split('T')[0],
+ isFeatured: false, allowContact: false
});
+ const [selectedImages, setSelectedImages] = useState([]);
+ const [imagePreviews, setImagePreviews] = useState([]);
+ const fileInputRef = useRef(null);
const [options, setOptions] = useState({
isBold: false, isFrame: false, fontSize: 'normal', alignment: 'left'
@@ -233,13 +244,13 @@ export default function FastEntryPage() {
days: formData.days,
isBold: options.isBold,
isFrame: options.isFrame,
- startDate: new Date().toISOString()
+ startDate: formData.startDate || new Date().toISOString()
});
setPricing(res.data);
} catch (error) { console.error(error); }
};
calculatePrice();
- }, [debouncedText, formData.categoryId, formData.days, options]);
+ }, [debouncedText, formData.categoryId, formData.days, options, formData.startDate]);
useEffect(() => {
if (debouncedClientSearch.length > 2 && showSuggestions) {
@@ -251,17 +262,17 @@ export default function FastEntryPage() {
const handlePaymentConfirm = async (payments: Payment[]) => {
try {
- const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1);
- await api.post('/listings', {
+ const listingRes = await api.post('/listings', {
categoryId: parseInt(formData.categoryId),
operationId: parseInt(formData.operationId),
- title: formData.text.substring(0, 40) + '...',
+ title: formData.title || (formData.text.substring(0, 40) + '...'),
description: formData.text,
- price: 0,
+ price: parseFloat(formData.price) || 0,
adFee: pricing.totalPrice,
status: 'Published',
+ origin: 'Mostrador',
printText: formData.text,
- printStartDate: tomorrow.toISOString(),
+ printStartDate: formData.startDate,
printDaysCount: formData.days,
isBold: options.isBold,
isFrame: options.isFrame,
@@ -269,12 +280,41 @@ export default function FastEntryPage() {
printAlignment: options.alignment,
clientName: formData.clientName,
clientDni: formData.clientDni,
+ publicationStartDate: formData.startDate,
+ isFeatured: formData.isFeatured,
+ allowContact: formData.allowContact,
payments
});
+
+ const listingId = listingRes.data.id;
+
+ // Subir imágenes si existen
+ if (selectedImages.length > 0) {
+ for (const file of selectedImages) {
+ const imgFormData = new FormData();
+ imgFormData.append('file', file);
+ await api.post(`/images/upload/${listingId}`, imgFormData, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ });
+ }
+ }
+
printCourtesyTicket(formData, pricing);
setTimeout(() => { printPaymentReceipt(formData, pricing, payments); }, 500);
- setFormData({ ...formData, text: '', clientName: '', clientDni: '' });
+ setFormData({
+ ...formData,
+ text: '',
+ title: '',
+ price: '',
+ clientName: '',
+ clientDni: '',
+ startDate: new Date(Date.now() + 86400000).toISOString().split('T')[0],
+ isFeatured: false,
+ allowContact: false
+ });
setOptions({ isBold: false, isFrame: false, fontSize: 'normal', alignment: 'left' });
+ setSelectedImages([]);
+ setImagePreviews([]);
setShowPaymentModal(false);
setErrors({});
showToast('Aviso procesado correctamente.', 'success');
@@ -288,6 +328,21 @@ export default function FastEntryPage() {
setShowSuggestions(false);
};
+ const handleImageChange = (e: React.ChangeEvent) => {
+ if (e.target.files) {
+ const files = Array.from(e.target.files);
+ setSelectedImages(prev => [...prev, ...files]);
+
+ const newPreviews = files.map(file => URL.createObjectURL(file));
+ setImagePreviews(prev => [...prev, ...newPreviews]);
+ }
+ };
+
+ const removeImage = (index: number) => {
+ setSelectedImages(prev => prev.filter((_, i) => i !== index));
+ setImagePreviews(prev => prev.filter((_, i) => i !== index));
+ };
+
if (sessionLoading) {
return (
@@ -299,12 +354,11 @@ export default function FastEntryPage() {
return (
<>
- {/* BLOQUEO DE SEGURIDAD: Si no hay sesión, mostramos el modal de apertura */}
{!session.isOpen && (
navigate('/dashboard')} // Si cancela la apertura, lo sacamos de "Nuevo Aviso"
+ onCancel={() => navigate('/dashboard')}
/>
)}
@@ -314,17 +368,14 @@ export default function FastEntryPage() {
animate={{
opacity: 1,
scale: 1,
- // Si no hay sesión, aplicamos un filtro de desenfoque y desaturación
filter: session.isOpen ? "blur(0px) grayscale(0)" : "blur(8px) grayscale(1)"
}}
transition={{ duration: 0.7, ease: "easeInOut" }}
className={clsx(
"w-full h-full p-5 flex gap-5 bg-slate-50/50 overflow-hidden max-h-screen",
- // Bloqueamos interacciones físicas mientras el modal esté presente
!session.isOpen && "pointer-events-none select-none opacity-40"
)}
>
- {/* PANEL IZQUIERDO: FORMULARIO */}
@@ -337,7 +388,7 @@ export default function FastEntryPage() {
-
+
setIsCatDropdownOpen(!isCatDropdownOpen)}
@@ -376,42 +427,84 @@ export default function FastEntryPage() {
-
-
-
-
-
-
+
+
+
+ setFormData({ ...formData, title: e.target.value })}
+ />
-
-
-
Palabras 0 ? "text-blue-400" : "text-slate-700")}>{pricing.wordCount.toString().padStart(2, '0')}
-
Signos Especiales 0 ? "text-amber-400" : "text-slate-700")}>{pricing.specialCharCount.toString().padStart(2, '0')}
+
+
+
+ $
+ setFormData({ ...formData, price: e.target.value })}
+ />
-
Vista optimizada para diario{[1, 2, 3, 4, 5].map(i =>
i * 15 ? "bg-blue-500" : "bg-slate-800")}>
)}
-
-
+
+
+
+
+
+
+
+
Palabras 0 ? "text-blue-400" : "text-slate-700")}>{pricing.wordCount.toString().padStart(2, '0')}
+
Signos 0 ? "text-amber-400" : "text-slate-700")}>{pricing.specialCharCount.toString().padStart(2, '0')}
+
+
+
Vista Diario
+
+ {[1, 2, 3, 4, 5].map(i =>
i * 15 ? "bg-blue-500" : "bg-slate-800")}>
)}
+
+
+
+
+
+
+
+
+
+
+ setFormData({ ...formData, startDate: e.target.value })}
+ />
+
+
+
-
-
+
-
-
+
+
+
setFormData({ ...formData, isFeatured: !formData.isFeatured })}
+ >
+
+
+
+
+ Aviso Destacado
+ Aparece primero en la web
+
+
+
+
setFormData({ ...formData, allowContact: !formData.allowContact })}
+ >
+
+
+
+
+ Permitir Contacto
+ Habilita botón de mensajes
+
@@ -479,18 +600,57 @@ export default function FastEntryPage() {
+
+
+
+
+ Multimedia
+
+ {selectedImages.length} fotos
+
+
+
+ {imagePreviews.map((url, i) => (
+
+

+
+
+ ))}
+
+
+
+
+
-
-
- {showPaymentModal && (
-
setShowPaymentModal(false)} />
- )}
+
+ {showPaymentModal && (
+ setShowPaymentModal(false)} />
+ )}
>
);
}
\ No newline at end of file
diff --git a/frontend/public-web/.gitignore b/frontend/public-web/.gitignore
index a547bf3..ac66765 100644
--- a/frontend/public-web/.gitignore
+++ b/frontend/public-web/.gitignore
@@ -12,6 +12,11 @@ dist
dist-ssr
*.local
+# Carpeta de Subidas
+# ---------------------
+uploads/
+
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
diff --git a/src/SIGCM.API/Controllers/ClientsController.cs b/src/SIGCM.API/Controllers/ClientsController.cs
index 2e7b3b1..8aaa73a 100644
--- a/src/SIGCM.API/Controllers/ClientsController.cs
+++ b/src/SIGCM.API/Controllers/ClientsController.cs
@@ -27,9 +27,9 @@ public class ClientsController : ControllerBase
}
[HttpGet]
- public async Task GetAll()
+ public async Task GetAll([FromQuery] string? q = null)
{
- var clients = await _repo.GetAllWithStatsAsync();
+ var clients = await _repo.GetAllWithStatsAsync(q);
return Ok(clients);
}
@@ -64,4 +64,30 @@ public class ClientsController : ControllerBase
if (summary == null) return NotFound();
return Ok(summary);
}
+
+ [HttpPost("{id}/reset-password")]
+ [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Admin")]
+ public async Task ResetPassword(int id)
+ {
+ // Establecemos 1234 como clave por defecto para el blanqueo
+ var passwordHash = BCrypt.Net.BCrypt.HashPassword("1234");
+ await _repo.ResetPasswordAsync(id, passwordHash);
+
+ // Audit Log
+ var userIdClaim = User.FindFirst("Id")?.Value;
+ if (int.TryParse(userIdClaim, out int userId))
+ {
+ await _auditRepo.AddLogAsync(new AuditLog
+ {
+ UserId = userId,
+ Action = "RESET_CLIENT_PASSWORD",
+ EntityId = id,
+ EntityType = "User",
+ Details = "Contraseña de cliente restablecida a '1234' por el administrador.",
+ CreatedAt = DateTime.UtcNow
+ });
+ }
+
+ return Ok(new { message = "Contraseña restablecida a '1234' correctamente." });
+ }
}
\ No newline at end of file
diff --git a/src/SIGCM.API/Controllers/CouponsController.cs b/src/SIGCM.API/Controllers/CouponsController.cs
index 9fa0201..d25860d 100644
--- a/src/SIGCM.API/Controllers/CouponsController.cs
+++ b/src/SIGCM.API/Controllers/CouponsController.cs
@@ -8,7 +8,6 @@ namespace SIGCM.API.Controllers;
[ApiController]
[Route("api/[controller]")]
-[Authorize(Roles = "Admin")]
public class CouponsController : ControllerBase
{
private readonly ICouponRepository _repository;
@@ -21,13 +20,35 @@ public class CouponsController : ControllerBase
}
[HttpGet]
+ [Authorize(Roles = "Admin")]
public async Task GetAll()
{
var coupons = await _repository.GetAllAsync();
return Ok(coupons);
}
+ [HttpGet("validate/{code}")]
+ [Authorize]
+ public async Task GetByCode(string code)
+ {
+ var coupon = await _repository.GetByCodeAsync(code);
+ if (coupon == null || !coupon.IsActive)
+ return NotFound(new { message = "Cupón inválido o inactivo." });
+
+ if (coupon.ExpiryDate.HasValue && coupon.ExpiryDate.Value < DateTime.UtcNow)
+ return BadRequest(new { message = "El cupón ha expirado." });
+
+ if (coupon.MaxUsages.HasValue)
+ {
+ // Note: We might need a method to count total usages if we want strictly enforce total max usages here
+ // but for now, we return the coupon data so the frontend can display info.
+ }
+
+ return Ok(coupon);
+ }
+
[HttpPost]
+ [Authorize(Roles = "Admin")]
public async Task Create(CreateCouponDto dto)
{
// Simple manual mapping
@@ -63,6 +84,7 @@ public class CouponsController : ControllerBase
}
[HttpDelete("{id}")]
+ [Authorize(Roles = "Admin")]
public async Task Delete(int id)
{
await _repository.DeleteAsync(id);
diff --git a/src/SIGCM.API/Controllers/ListingsController.cs b/src/SIGCM.API/Controllers/ListingsController.cs
index d7ec418..0bd0933 100644
--- a/src/SIGCM.API/Controllers/ListingsController.cs
+++ b/src/SIGCM.API/Controllers/ListingsController.cs
@@ -43,25 +43,37 @@ public class ListingsController : ControllerBase
int? clientId = null;
var user = await _userRepo.GetByIdAsync(currentUserId);
- // Lógica de Vinculación Usuario-Cliente
- if (user != null)
+ // Lógica de Vinculación Distinguida (Web vs Mostrador)
+ if (dto.Origin == "Mostrador")
{
- clientId = user.ClientId;
-
- // Si el usuario no tiene cliente vinculado, pero envía datos de facturación (o registramos los que tiene)
- if (clientId == null)
+ if (!string.IsNullOrWhiteSpace(dto.ClientDni))
{
- string? dniToUse = dto.ClientDni ?? user.BillingTaxId;
- if (!string.IsNullOrWhiteSpace(dniToUse))
+ // En mostrador buscamos/creamos al cliente por su DNI, pero NO lo vinculamos al cajero
+ clientId = await _clientRepo.EnsureClientExistsAsync(dto.ClientName ?? "S/D", dto.ClientDni);
+ }
+ }
+ else
+ {
+ // Lógica para usuarios Web (Auto-servicio)
+ if (user != null)
+ {
+ clientId = user.ClientId;
+
+ // Si el usuario no tiene cliente vinculado, pero envía datos de facturación, lo vinculamos
+ if (clientId == null)
{
- string? nameToUse = dto.ClientName ?? user.BillingName ?? user.Username;
- clientId = await _clientRepo.EnsureClientExistsAsync(nameToUse!, dniToUse!);
-
- // Actualizamos el usuario con su nuevo nexo de cliente
- user.ClientId = clientId;
- user.BillingTaxId = dniToUse;
- user.BillingName = nameToUse;
- await _userRepo.UpdateAsync(user);
+ string? dniToUse = dto.ClientDni ?? user.BillingTaxId;
+ if (!string.IsNullOrWhiteSpace(dniToUse))
+ {
+ string? nameToUse = dto.ClientName ?? user.BillingName ?? user.Username;
+ clientId = await _clientRepo.EnsureClientExistsAsync(nameToUse!, dniToUse!);
+
+ // Actualizamos el usuario auto-servicio con su nuevo nexo de cliente
+ user.ClientId = clientId;
+ user.BillingTaxId = dniToUse;
+ user.BillingName = nameToUse;
+ await _userRepo.UpdateAsync(user);
+ }
}
}
}
diff --git a/src/SIGCM.API/Controllers/UsersController.cs b/src/SIGCM.API/Controllers/UsersController.cs
index 124d8aa..711241e 100644
--- a/src/SIGCM.API/Controllers/UsersController.cs
+++ b/src/SIGCM.API/Controllers/UsersController.cs
@@ -25,9 +25,9 @@ public class UsersController : ControllerBase
public async Task GetAll()
{
var users = await _repository.GetAllAsync();
- // Excluimos clientes para que solo aparezcan en su propio gestor
+ // Excluimos tanto clientes de mostrador como usuarios web para que solo aparezcan en su propio gestor
var sanitized = users
- .Where(u => u.Role != "Client")
+ .Where(u => u.Role != "Client" && u.Role != "User")
.Select(u => new {
u.Id, u.Username, u.Role, u.Email, u.CreatedAt
});
diff --git a/src/SIGCM.Domain/Entities/Client.cs b/src/SIGCM.Domain/Entities/Client.cs
index 58e2474..a213f56 100644
--- a/src/SIGCM.Domain/Entities/Client.cs
+++ b/src/SIGCM.Domain/Entities/Client.cs
@@ -8,4 +8,8 @@ public class Client
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Address { get; set; }
+ public string? TaxType { get; set; }
+ public string? Username { get; set; }
+ public bool IsActive { get; set; }
+ public string? Role { get; set; }
}
\ No newline at end of file
diff --git a/src/SIGCM.Infrastructure/Data/DbInitializer.cs b/src/SIGCM.Infrastructure/Data/DbInitializer.cs
index db968d3..ef8f713 100644
--- a/src/SIGCM.Infrastructure/Data/DbInitializer.cs
+++ b/src/SIGCM.Infrastructure/Data/DbInitializer.cs
@@ -291,6 +291,27 @@ END
";
await connection.ExecuteAsync(dataMigrationSql);
+ // --- NORMALIZACIÓN DE CLIENTES SIN CLAVE (Usuario corto y clave 1234) ---
+ var upgradeClientsSql = @"
+ -- Juan Perez
+ UPDATE Users SET Username = 'jperez', PasswordHash = '$2a$11$EaJecdmgKfGhOUEmMzT/I.hD/9WES3GehL72xXyc2stXC26ncLNsO', MustChangePassword = 1
+ WHERE Id = 1003 AND PasswordHash = 'N/A';
+
+ -- Maria Rodriguez
+ UPDATE Users SET Username = 'mrodriguez', PasswordHash = '$2a$11$EaJecdmgKfGhOUEmMzT/I.hD/9WES3GehL72xXyc2stXC26ncLNsO', MustChangePassword = 1
+ WHERE Id = 1004 AND PasswordHash = 'N/A';
+
+ -- Inmobiliaria City Bell
+ UPDATE Users SET Username = 'inmocity', PasswordHash = '$2a$11$EaJecdmgKfGhOUEmMzT/I.hD/9WES3GehL72xXyc2stXC26ncLNsO', MustChangePassword = 1
+ WHERE Id = 1005 AND PasswordHash = 'N/A';
+
+ -- General para otros clientes migrados (si hubiera)
+ UPDATE Users
+ SET PasswordHash = '$2a$11$EaJecdmgKfGhOUEmMzT/I.hD/9WES3GehL72xXyc2stXC26ncLNsO', MustChangePassword = 1
+ WHERE PasswordHash = 'N/A' AND Role = 'Client';
+ ";
+ await connection.ExecuteAsync(upgradeClientsSql);
+
// --- SEED DE DATOS (Usuario Admin) ---
var adminCount = await connection.ExecuteScalarAsync("SELECT COUNT(1) FROM Users WHERE Username = 'admin'");
diff --git a/src/SIGCM.Infrastructure/Repositories/ClientRepository.cs b/src/SIGCM.Infrastructure/Repositories/ClientRepository.cs
index a2ddf90..f3b64be 100644
--- a/src/SIGCM.Infrastructure/Repositories/ClientRepository.cs
+++ b/src/SIGCM.Infrastructure/Repositories/ClientRepository.cs
@@ -22,7 +22,8 @@ public class ClientRepository
Id,
ISNULL(BillingName, Username) as Name,
ISNULL(BillingTaxId, '') as DniOrCuit,
- Email, Phone, BillingAddress as Address
+ Email, Phone, BillingAddress as Address, BillingTaxType as TaxType,
+ Username, IsActive, Role
FROM Users
WHERE BillingName LIKE @Query OR BillingTaxId LIKE @Query OR Username LIKE @Query
ORDER BY BillingName";
@@ -54,23 +55,34 @@ public class ClientRepository
}
}
- // Obtener todos con estadísticas desde Users
- public async Task> GetAllWithStatsAsync()
+ // Obtener con estadísticas desde Users (con filtro y límite para performance)
+ public async Task> GetAllWithStatsAsync(string? searchTerm = null)
{
using var conn = _db.CreateConnection();
var sql = @"
- SELECT
+ SELECT TOP 100
u.Id as id,
ISNULL(u.BillingName, u.Username) as name,
ISNULL(u.BillingTaxId, 'S/D') as dniOrCuit,
ISNULL(u.Email, 'Sin correo') as email,
ISNULL(u.Phone, 'Sin teléfono') as phone,
+ ISNULL(u.BillingTaxType, 'Consumidor Final') as taxType,
+ u.Username as username,
+ u.IsActive as isActive,
+ u.Role as role,
(SELECT COUNT(1) FROM Listings l WHERE l.ClientId = u.Id) as totalAds,
ISNULL((SELECT SUM(AdFee) FROM Listings l WHERE l.ClientId = u.Id), 0) as totalSpent
FROM Users u
- WHERE Role IN ('Client', 'User') -- Mostramos tanto clientes puros como usuarios web
- ORDER BY name";
- return await conn.QueryAsync(sql);
+ WHERE Role IN ('Client', 'User')";
+
+ if (!string.IsNullOrWhiteSpace(searchTerm))
+ {
+ sql += " AND (u.BillingName LIKE @Query OR u.BillingTaxId LIKE @Query OR u.Username LIKE @Query)";
+ }
+
+ sql += " ORDER BY name";
+
+ return await conn.QueryAsync(sql, new { Query = $"%{searchTerm}%" });
}
public async Task UpdateAsync(Client client)
@@ -82,7 +94,10 @@ public class ClientRepository
BillingTaxId = @DniOrCuit,
Email = @Email,
Phone = @Phone,
- BillingAddress = @Address
+ BillingAddress = @Address,
+ BillingTaxType = @TaxType,
+ Username = @Username,
+ IsActive = @IsActive
WHERE Id = @Id";
await conn.ExecuteAsync(sql, client);
}
@@ -95,6 +110,8 @@ public class ClientRepository
u.Id,
ISNULL(u.BillingName, u.Username) as Name,
u.BillingTaxId as DniOrCuit, u.Email, u.Phone, u.BillingAddress as Address,
+ u.BillingTaxType as TaxType,
+ u.Username, u.IsActive, u.Role,
(SELECT COUNT(1) FROM Listings WHERE ClientId = u.Id) as TotalAds,
ISNULL((SELECT SUM(AdFee) FROM Listings WHERE ClientId = u.Id), 0) as TotalInvested,
(SELECT MAX(CreatedAt) FROM Listings WHERE ClientId = u.Id) as LastAdDate,
@@ -112,4 +129,11 @@ public class ClientRepository
return await conn.QuerySingleOrDefaultAsync(sql, new { Id = clientId });
}
+
+ public async Task ResetPasswordAsync(int clientId, string passwordHash)
+ {
+ using var conn = _db.CreateConnection();
+ var sql = "UPDATE Users SET PasswordHash = @Hash, MustChangePassword = 1 WHERE Id = @Id";
+ await conn.ExecuteAsync(sql, new { Hash = passwordHash, Id = clientId });
+ }
}
\ No newline at end of file
diff --git a/src/SIGCM.Infrastructure/Repositories/ListingRepository.cs b/src/SIGCM.Infrastructure/Repositories/ListingRepository.cs
index 5c2bbaa..ffb64f7 100644
--- a/src/SIGCM.Infrastructure/Repositories/ListingRepository.cs
+++ b/src/SIGCM.Infrastructure/Repositories/ListingRepository.cs
@@ -192,11 +192,15 @@ public class ListingRepository : IListingRepository
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
+ DECLARE @CId INT = (SELECT ClientId FROM Users WHERE Id = @UserId);
+
SELECT l.*, c.Name as CategoryName,
(SELECT TOP 1 Url FROM ListingImages li WHERE li.ListingId = l.Id ORDER BY IsMainInfo DESC, DisplayOrder ASC) as MainImageUrl
FROM Listings l
JOIN Categories c ON l.CategoryId = c.Id
- WHERE l.UserId = @UserId
+ WHERE l.UserId = @UserId
+ OR l.ClientId = @UserId
+ OR (l.ClientId = @CId AND l.ClientId IS NOT NULL)
ORDER BY l.CreatedAt DESC";
return await conn.QueryAsync(sql, new { UserId = userId });