Employee leaving date fixed.

Image now showing in vouchers fixed.
This commit is contained in:
2026-09-03 18:53:16 +00:00
parent 76975d88f3
commit a06d18d93c
14 changed files with 1568 additions and 177 deletions
+77 -22
View File
@@ -3,6 +3,9 @@ import multiprocessing
import os
# --- Worker sizing -----------------------------------------------------------
# Prefer explicit WEB_CONCURRENCY in containers. Otherwise fall back to
# WORKERS_PER_CORE * cpu_count(), with a minimum of 2 and optional MAX_WORKERS.
workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
max_workers_str = os.getenv("MAX_WORKERS")
use_max_workers = None
@@ -10,12 +13,6 @@ if max_workers_str:
use_max_workers = int(max_workers_str)
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)
host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "9994")
bind_env = os.getenv("BIND", None)
use_loglevel = os.getenv("LOG_LEVEL", "info")
use_bind = bind_env if bind_env else f"{host}:{port}"
cores = multiprocessing.cpu_count()
workers_per_core = float(workers_per_core_str)
default_web_concurrency = workers_per_core * cores
@@ -26,27 +23,53 @@ else:
web_concurrency = max(int(default_web_concurrency), 2)
if use_max_workers:
web_concurrency = min(web_concurrency, use_max_workers)
accesslog_var = os.getenv("ACCESS_LOG", "-")
use_accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
use_errorlog = errorlog_var or None
graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
timeout_str = os.getenv("TIMEOUT", "120")
keepalive_str = os.getenv("KEEP_ALIVE", "5")
# Gunicorn config variables
loglevel = use_loglevel
# --- Bind / logging ----------------------------------------------------------
host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "9994")
bind_env = os.getenv("BIND", None)
use_bind = bind_env if bind_env else f"{host}:{port}"
# Tell Gunicorn to trust X-Forwarded-For headers from the reverse proxy
forwarded_allow_ips = "*"
# Optional: Ensure proxy protocol is recognized
proxy_allow_ips = "*"
loglevel = os.getenv("LOG_LEVEL", "info")
accesslog_var = os.getenv("ACCESS_LOG", "-")
accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
errorlog = errorlog_var or None
worker_tmp_dir = "/dev/shm"
# --- Timeouts / keepalive ----------------------------------------------------
graceful_timeout = int(os.getenv("GRACEFUL_TIMEOUT", "120"))
timeout = int(os.getenv("TIMEOUT", "120"))
keepalive = int(os.getenv("KEEP_ALIVE", "5"))
# --- Robustness knobs (recommended) -----------------------------------------
# Recycle workers gradually to mitigate memory leaks / fragmentation without
# full container restarts. Tune via env if needed.
max_requests = int(os.getenv("MAX_REQUESTS", "10000"))
max_requests_jitter = int(os.getenv("MAX_REQUESTS_JITTER", "1000"))
# Prevent slow clients from holding connections forever (defaults are fine).
# You can tune these via env if you ever need to.
# worker_connections matters only for async worker types; kept here for clarity.
worker_connections = int(os.getenv("WORKER_CONNECTIONS", "1000"))
# Helpful in containerized environments: ensure workers are responsive.
# (Defaults are okay; leaving commented unless you want strict behavior.)
# heartbeat_interval = int(os.getenv("HEARTBEAT_INTERVAL", "30"))
# --- Gunicorn config vars ----------------------------------------------------
workers = web_concurrency
bind = use_bind
errorlog = use_errorlog
worker_tmp_dir = "/dev/shm"
accesslog = use_accesslog
graceful_timeout = int(graceful_timeout_str)
timeout = int(timeout_str)
keepalive = int(keepalive_str)
# For debugging and testing
# --- Debug print (keep if you like) ------------------------------------------
log_data = {
"loglevel": loglevel,
"workers": workers,
@@ -56,10 +79,42 @@ log_data = {
"keepalive": keepalive,
"errorlog": errorlog,
"accesslog": accesslog,
"worker_tmp_dir": worker_tmp_dir,
"max_requests": max_requests,
"max_requests_jitter": max_requests_jitter,
"worker_connections": worker_connections,
# Additional, non-gunicorn variables
"workers_per_core": workers_per_core,
"use_max_workers": use_max_workers,
"host": host,
"port": port,
"cores": cores,
}
print(json.dumps(log_data)) # noqa: T201
# Variables Gunicorn reads
# (these must be module-level names)
# fmt: off
# Core
loglevel = loglevel
workers = workers
bind = bind
# Logging
accesslog = accesslog
errorlog = errorlog
# Runtime
worker_tmp_dir = worker_tmp_dir
graceful_timeout = graceful_timeout
timeout = timeout
keepalive = keepalive
# Recycling
max_requests = max_requests
max_requests_jitter = max_requests_jitter
# Concurrency (relevant for some worker types; harmless otherwise)
worker_connections = worker_connections
# fmt: on
@@ -64,7 +64,7 @@
<mat-datepicker-toggle matSuffix [for]="joiningDate"></mat-datepicker-toggle>
<mat-datepicker #joiningDate></mat-datepicker>
</mat-form-field>
@if (!item().isActive) {
@if (!form.isActive().value()) {
<mat-form-field class="flex-auto">
<mat-label>Leaving Date</mat-label>
<input matInput [matDatepicker]="leavingDate" [formField]="form.leavingDate" autocomplete="off" />
@@ -76,7 +76,7 @@
</form>
<div class="row-container">
<button mat-raised-button color="primary" (click)="save()">Save</button>
<button mat-raised-button color="primary" [disabled]="form().invalid()" (click)="save()">Save</button>
@if (!!item().id) {
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
}
@@ -1,5 +1,5 @@
import { Component, inject, afterNextRender, linkedSignal, input, computed } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { form as createForm, FormField, FormRoot, required } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core';
@@ -28,8 +28,8 @@ export interface EmployeeDetailFormData {
points: number;
isActive: boolean;
costCentre: string;
joiningDate: string;
leavingDate: string | null;
joiningDate: Date;
leavingDate: Date | null;
}
@Component({
@@ -70,12 +70,22 @@ export class EmployeeDetailComponent {
points: itemVal.points || 0,
isActive: itemVal.isActive ?? true,
costCentre: itemVal.costCentre?.id ?? '',
joiningDate: itemVal.joiningDate,
leavingDate: itemVal.leavingDate,
joiningDate: itemVal.joiningDate ? moment(itemVal.joiningDate, 'DD-MMM-YYYY').toDate() : new Date(),
leavingDate: itemVal.leavingDate ? moment(itemVal.leavingDate, 'DD-MMM-YYYY').toDate() : null,
}),
});
form = createForm(this.model);
form = createForm(this.model, (f) => {
required(f.name);
required(f.designation);
required(f.salary);
required(f.points);
required(f.joiningDate);
required(f.leavingDate, {
when: ({ valueOf }) => !valueOf(f.isActive),
message: 'Leaving date is required',
});
});
costCentresResource = this.costCentreService.list();
costCentres = computed(() => this.costCentresResource.value() || []);
@@ -87,6 +97,11 @@ export class EmployeeDetailComponent {
}
save() {
if (this.form().invalid()) {
const errorMsg = this.form().errorSummary()[0]?.message || 'Leaving date is required';
this.snackBar.open(errorMsg, 'Danger');
return;
}
this.ser.saveOrUpdate(this.getItem()).subscribe({
next: () => {
this.snackBar.open('', 'Success');
@@ -133,7 +148,8 @@ export class EmployeeDetailComponent {
item.isActive = formValue.isActive;
item.costCentre.id = formValue.costCentre ?? '';
item.joiningDate = moment(formValue.joiningDate).format('DD-MMM-YYYY');
item.leavingDate = item.isActive ? null : moment(formValue.leavingDate).format('DD-MMM-YYYY');
item.leavingDate =
item.isActive || !formValue.leavingDate ? null : moment(formValue.leavingDate).format('DD-MMM-YYYY');
return item;
}
}
+230 -7
View File
@@ -16,19 +16,242 @@
justify-content: center;
}
.img-container {
/* Attachments Section */
.attachments-section {
display: flex;
flex-direction: column;
gap: 10px;
margin: 6px 0 16px;
}
.attachments-header {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
color: var(--mat-sys-on-surface-variant, #64748b);
user-select: none;
}
.attachments-header-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-primary, #0284c7);
}
.attachments-badge {
font-size: 11px;
font-weight: 600;
padding: 1px 7px;
border-radius: 9999px;
background: var(--mat-sys-secondary-container, #e2e8f0);
color: var(--mat-sys-on-secondary-container, #1e293b);
}
.attachments-grid {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
}
.attachment-card {
position: relative;
width: 110px;
height: 155px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
background: var(--mat-sys-surface, #ffffff);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.img-container .overlay {
display: none;
.attachment-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.img-container:hover > .overlay {
display: inline-block;
.thumbnail-wrapper {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
outline: none;
}
.thumbnail-wrapper:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: -2px;
}
.thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.25s ease;
}
.thumbnail-wrapper:hover .thumbnail-img {
transform: scale(1.05);
}
.zoom-overlay {
position: absolute;
left: 60px;
top: 0;
inset: 0;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.thumbnail-wrapper:hover .zoom-overlay,
.thumbnail-wrapper:focus-visible .zoom-overlay {
opacity: 1;
}
.zoom-icon {
color: #ffffff;
font-size: 28px;
width: 28px;
height: 28px;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.5));
}
.attachment-card .delete-btn {
position: absolute;
top: 6px;
right: 6px;
width: 32px;
height: 32px;
min-width: 32px;
padding: 0;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
color: #dc2626;
opacity: 0;
transition:
opacity 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
transform 0.15s ease;
z-index: 3;
display: inline-flex;
align-items: center;
justify-content: center;
}
.attachment-card:hover .delete-btn,
.attachment-card:focus-within .delete-btn {
opacity: 1;
}
.attachment-card .delete-btn:hover,
.attachment-card .delete-btn:focus-visible {
background: #dc2626;
color: #ffffff;
transform: scale(1.08);
}
.attachment-card .delete-btn mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
line-height: 18px;
}
.upload-tile {
width: 110px;
height: 155px;
border-radius: 12px;
border: 2px dashed var(--mat-sys-outline, #cbd5e1);
background: var(--mat-sys-surface-container-low, #f8fafc);
cursor: pointer;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
transform 0.2s ease;
}
.upload-tile:hover {
border-color: var(--mat-sys-primary, #0284c7);
background: rgba(2, 132, 199, 0.05);
transform: translateY(-2px);
}
.upload-tile:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: 2px;
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 8px;
gap: 6px;
}
.upload-icon-circle {
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--mat-sys-primary-container, #e0f2fe);
color: var(--mat-sys-on-primary-container, #0284c7);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.upload-tile:hover .upload-icon-circle {
transform: scale(1.08);
}
.upload-icon-circle mat-icon {
font-size: 22px;
width: 22px;
height: 22px;
line-height: 22px;
}
.upload-title {
font-size: 12px;
font-weight: 600;
color: var(--mat-sys-on-surface, #1e293b);
}
.upload-subtitle {
font-size: 10px;
color: var(--mat-sys-on-surface-variant, #64748b);
}
.file-input-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.gold {
+63 -22
View File
@@ -131,29 +131,70 @@
</mat-autocomplete>
</mat-form-field>
</div>
<div class="row-container">
@for (item of form.files().value(); track item) {
<div class="img-container" class="flex-auto basis-1-5">
<img
[src]="item.thumbnail"
alt="img"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
/>
<button mat-icon-button class="overlay" (click)="deleteImage(item)">
<mat-icon>delete</mat-icon>
</button>
</div>
}
<div class="img-container" class="flex flex-auto basis-1-5">
<label ng-href for="fileUp">
<img
alt="add"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTUwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjE1MCIgZmlsbD0iI2VlZSIvPjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjUwIiB5PSI3NSIgc3R5bGU9ImZpbGw6I2FhYTtmb250LXdlaWdodDpib2xkO2ZvbnQtc2l6ZToxMDBweDtmb250LWZhbWlseTpBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtkb21pbmFudC1iYXNlbGluZTpjZW50cmFsIj4rPC90ZXh0Pjwvc3ZnPg=="
/>
<div class="attachments-section">
<div class="attachments-header">
<mat-icon class="attachments-header-icon">attach_file</mat-icon>
<span class="attachments-title">Attachments</span>
@if (form.files().value().length > 0) {
<span class="attachments-badge">{{ form.files().value().length }}</span>
}
</div>
<div class="attachments-grid">
@for (item of form.files().value(); track item) {
<div class="attachment-card">
<div
class="thumbnail-wrapper"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
role="button"
aria-label="View enlarged attachment"
title="Click to preview"
>
<img [src]="item.thumbnail" alt="Journal attachment thumbnail" class="thumbnail-img" />
<div class="zoom-overlay">
<mat-icon class="zoom-icon">zoom_in</mat-icon>
</div>
</div>
<button
mat-icon-button
type="button"
class="delete-btn"
(click)="deleteImage(item); $event.stopPropagation()"
aria-label="Delete attachment"
title="Delete attachment"
>
<mat-icon>delete</mat-icon>
</button>
</div>
}
<label
for="fileUp"
class="upload-tile"
tabindex="0"
(keydown.enter)="fileInput.click()"
role="button"
aria-label="Upload image attachments"
>
<div class="upload-content">
<div class="upload-icon-circle">
<mat-icon>add_photo_alternate</mat-icon>
</div>
<span class="upload-title">Add Image</span>
<span class="upload-subtitle">Click to upload</span>
</div>
</label>
<input type="file" id="fileUp" multiple accept="image/*" style="display: none" (change)="detectFiles($event)" />
<input
#fileInput
type="file"
id="fileUp"
multiple
accept="image/*"
class="file-input-hidden"
(change)="detectFiles($event)"
/>
</div>
</div>
}
+230 -7
View File
@@ -16,19 +16,242 @@
justify-content: center;
}
.img-container {
/* Attachments Section */
.attachments-section {
display: flex;
flex-direction: column;
gap: 10px;
margin: 6px 0 16px;
}
.attachments-header {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
color: var(--mat-sys-on-surface-variant, #64748b);
user-select: none;
}
.attachments-header-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-primary, #0284c7);
}
.attachments-badge {
font-size: 11px;
font-weight: 600;
padding: 1px 7px;
border-radius: 9999px;
background: var(--mat-sys-secondary-container, #e2e8f0);
color: var(--mat-sys-on-secondary-container, #1e293b);
}
.attachments-grid {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
}
.attachment-card {
position: relative;
width: 110px;
height: 155px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
background: var(--mat-sys-surface, #ffffff);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.img-container .overlay {
display: none;
.attachment-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.img-container:hover > .overlay {
display: inline-block;
.thumbnail-wrapper {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
outline: none;
}
.thumbnail-wrapper:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: -2px;
}
.thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.25s ease;
}
.thumbnail-wrapper:hover .thumbnail-img {
transform: scale(1.05);
}
.zoom-overlay {
position: absolute;
left: 60px;
top: 0;
inset: 0;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.thumbnail-wrapper:hover .zoom-overlay,
.thumbnail-wrapper:focus-visible .zoom-overlay {
opacity: 1;
}
.zoom-icon {
color: #ffffff;
font-size: 28px;
width: 28px;
height: 28px;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.5));
}
.attachment-card .delete-btn {
position: absolute;
top: 6px;
right: 6px;
width: 32px;
height: 32px;
min-width: 32px;
padding: 0;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
color: #dc2626;
opacity: 0;
transition:
opacity 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
transform 0.15s ease;
z-index: 3;
display: inline-flex;
align-items: center;
justify-content: center;
}
.attachment-card:hover .delete-btn,
.attachment-card:focus-within .delete-btn {
opacity: 1;
}
.attachment-card .delete-btn:hover,
.attachment-card .delete-btn:focus-visible {
background: #dc2626;
color: #ffffff;
transform: scale(1.08);
}
.attachment-card .delete-btn mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
line-height: 18px;
}
.upload-tile {
width: 110px;
height: 155px;
border-radius: 12px;
border: 2px dashed var(--mat-sys-outline, #cbd5e1);
background: var(--mat-sys-surface-container-low, #f8fafc);
cursor: pointer;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
transform 0.2s ease;
}
.upload-tile:hover {
border-color: var(--mat-sys-primary, #0284c7);
background: rgba(2, 132, 199, 0.05);
transform: translateY(-2px);
}
.upload-tile:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: 2px;
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 8px;
gap: 6px;
}
.upload-icon-circle {
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--mat-sys-primary-container, #e0f2fe);
color: var(--mat-sys-on-primary-container, #0284c7);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.upload-tile:hover .upload-icon-circle {
transform: scale(1.08);
}
.upload-icon-circle mat-icon {
font-size: 22px;
width: 22px;
height: 22px;
line-height: 22px;
}
.upload-title {
font-size: 12px;
font-weight: 600;
color: var(--mat-sys-on-surface, #1e293b);
}
.upload-subtitle {
font-size: 10px;
color: var(--mat-sys-on-surface-variant, #64748b);
}
.file-input-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.gold {
+63 -22
View File
@@ -134,29 +134,70 @@
</mat-autocomplete>
</mat-form-field>
</div>
<div class="row-container">
@for (item of form.files().value(); track item) {
<div class="img-container" class="flex-auto basis-1-5">
<img
[src]="item.thumbnail"
alt="img"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
/>
<button mat-icon-button class="overlay" (click)="deleteImage(item)">
<mat-icon>delete</mat-icon>
</button>
</div>
}
<div class="img-container" class="flex flex-auto basis-1-5">
<label ng-href for="fileUp">
<img
alt="add"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTUwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjE1MCIgZmlsbD0iI2VlZSIvPjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjUwIiB5PSI3NSIgc3R5bGU9ImZpbGw6I2FhYTtmb250LXdlaWdodDpib2xkO2ZvbnQtc2l6ZToxMDBweDtmb250LWZhbWlseTpBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtkb21pbmFudC1iYXNlbGluZTpjZW50cmFsIj4rPC90ZXh0Pjwvc3ZnPg=="
/>
<div class="attachments-section">
<div class="attachments-header">
<mat-icon class="attachments-header-icon">attach_file</mat-icon>
<span class="attachments-title">Attachments</span>
@if (form.files().value().length > 0) {
<span class="attachments-badge">{{ form.files().value().length }}</span>
}
</div>
<div class="attachments-grid">
@for (item of form.files().value(); track item) {
<div class="attachment-card">
<div
class="thumbnail-wrapper"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
role="button"
aria-label="View enlarged attachment"
title="Click to preview"
>
<img [src]="item.thumbnail" alt="Attachment thumbnail" class="thumbnail-img" />
<div class="zoom-overlay">
<mat-icon class="zoom-icon">zoom_in</mat-icon>
</div>
</div>
<button
mat-icon-button
type="button"
class="delete-btn"
(click)="deleteImage(item); $event.stopPropagation()"
aria-label="Delete attachment"
title="Delete attachment"
>
<mat-icon>delete</mat-icon>
</button>
</div>
}
<label
for="fileUp"
class="upload-tile"
tabindex="0"
(keydown.enter)="fileInput.click()"
role="button"
aria-label="Upload image attachments"
>
<div class="upload-content">
<div class="upload-icon-circle">
<mat-icon>add_photo_alternate</mat-icon>
</div>
<span class="upload-title">Add Image</span>
<span class="upload-subtitle">Click to upload</span>
</div>
</label>
<input type="file" id="fileUp" multiple accept="image/*" style="display: none" (change)="detectFiles($event)" />
<input
#fileInput
type="file"
id="fileUp"
multiple
accept="image/*"
class="file-input-hidden"
(change)="detectFiles($event)"
/>
</div>
</div>
}
@@ -16,19 +16,242 @@
justify-content: center;
}
.img-container {
/* Attachments Section */
.attachments-section {
display: flex;
flex-direction: column;
gap: 10px;
margin: 6px 0 16px;
}
.attachments-header {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
color: var(--mat-sys-on-surface-variant, #64748b);
user-select: none;
}
.attachments-header-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-primary, #0284c7);
}
.attachments-badge {
font-size: 11px;
font-weight: 600;
padding: 1px 7px;
border-radius: 9999px;
background: var(--mat-sys-secondary-container, #e2e8f0);
color: var(--mat-sys-on-secondary-container, #1e293b);
}
.attachments-grid {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
}
.attachment-card {
position: relative;
width: 110px;
height: 155px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
background: var(--mat-sys-surface, #ffffff);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.img-container .overlay {
display: none;
.attachment-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.img-container:hover > .overlay {
display: inline-block;
.thumbnail-wrapper {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
outline: none;
}
.thumbnail-wrapper:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: -2px;
}
.thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.25s ease;
}
.thumbnail-wrapper:hover .thumbnail-img {
transform: scale(1.05);
}
.zoom-overlay {
position: absolute;
left: 60px;
top: 0;
inset: 0;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.thumbnail-wrapper:hover .zoom-overlay,
.thumbnail-wrapper:focus-visible .zoom-overlay {
opacity: 1;
}
.zoom-icon {
color: #ffffff;
font-size: 28px;
width: 28px;
height: 28px;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.5));
}
.attachment-card .delete-btn {
position: absolute;
top: 6px;
right: 6px;
width: 32px;
height: 32px;
min-width: 32px;
padding: 0;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
color: #dc2626;
opacity: 0;
transition:
opacity 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
transform 0.15s ease;
z-index: 3;
display: inline-flex;
align-items: center;
justify-content: center;
}
.attachment-card:hover .delete-btn,
.attachment-card:focus-within .delete-btn {
opacity: 1;
}
.attachment-card .delete-btn:hover,
.attachment-card .delete-btn:focus-visible {
background: #dc2626;
color: #ffffff;
transform: scale(1.08);
}
.attachment-card .delete-btn mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
line-height: 18px;
}
.upload-tile {
width: 110px;
height: 155px;
border-radius: 12px;
border: 2px dashed var(--mat-sys-outline, #cbd5e1);
background: var(--mat-sys-surface-container-low, #f8fafc);
cursor: pointer;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
transform 0.2s ease;
}
.upload-tile:hover {
border-color: var(--mat-sys-primary, #0284c7);
background: rgba(2, 132, 199, 0.05);
transform: translateY(-2px);
}
.upload-tile:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: 2px;
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 8px;
gap: 6px;
}
.upload-icon-circle {
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--mat-sys-primary-container, #e0f2fe);
color: var(--mat-sys-on-primary-container, #0284c7);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.upload-tile:hover .upload-icon-circle {
transform: scale(1.08);
}
.upload-icon-circle mat-icon {
font-size: 22px;
width: 22px;
height: 22px;
line-height: 22px;
}
.upload-title {
font-size: 12px;
font-weight: 600;
color: var(--mat-sys-on-surface, #1e293b);
}
.upload-subtitle {
font-size: 10px;
color: var(--mat-sys-on-surface-variant, #64748b);
}
.file-input-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.gold {
@@ -164,29 +164,70 @@
</mat-autocomplete>
</mat-form-field>
</div>
<div class="row-container">
@for (item of form.files().value(); track item) {
<div class="img-container" class="flex-auto basis-1-5">
<img
[src]="item.thumbnail"
alt="img"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
/>
<button mat-icon-button class="overlay" (click)="deleteImage(item)">
<mat-icon>delete</mat-icon>
</button>
</div>
}
<div class="img-container" class="flex flex-auto basis-1-5">
<label ng-href for="fileUp">
<img
alt="add"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTUwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjE1MCIgZmlsbD0iI2VlZSIvPjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjUwIiB5PSI3NSIgc3R5bGU9ImZpbGw6I2FhYTtmb250LXdlaWdodDpib2xkO2ZvbnQtc2l6ZToxMDBweDtmb250LWZhbWlseTpBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtkb21pbmFudC1iYXNlbGluZTpjZW50cmFsIj4rPC90ZXh0Pjwvc3ZnPg=="
/>
<div class="attachments-section">
<div class="attachments-header">
<mat-icon class="attachments-header-icon">attach_file</mat-icon>
<span class="attachments-title">Attachments</span>
@if (form.files().value().length > 0) {
<span class="attachments-badge">{{ form.files().value().length }}</span>
}
</div>
<div class="attachments-grid">
@for (item of form.files().value(); track item) {
<div class="attachment-card">
<div
class="thumbnail-wrapper"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
role="button"
aria-label="View enlarged attachment"
title="Click to preview"
>
<img [src]="item.thumbnail" alt="Attachment thumbnail" class="thumbnail-img" />
<div class="zoom-overlay">
<mat-icon class="zoom-icon">zoom_in</mat-icon>
</div>
</div>
<button
mat-icon-button
type="button"
class="delete-btn"
(click)="deleteImage(item); $event.stopPropagation()"
aria-label="Delete attachment"
title="Delete attachment"
>
<mat-icon>delete</mat-icon>
</button>
</div>
}
<label
for="fileUp"
class="upload-tile"
tabindex="0"
(keydown.enter)="fileInput.click()"
role="button"
aria-label="Upload image attachments"
>
<div class="upload-content">
<div class="upload-icon-circle">
<mat-icon>add_photo_alternate</mat-icon>
</div>
<span class="upload-title">Add Image</span>
<span class="upload-subtitle">Click to upload</span>
</div>
</label>
<input type="file" id="fileUp" multiple accept="image/*" style="display: none" (change)="detectFiles($event)" />
<input
#fileInput
type="file"
id="fileUp"
multiple
accept="image/*"
class="file-input-hidden"
(change)="detectFiles($event)"
/>
</div>
</div>
}
@@ -16,19 +16,242 @@
justify-content: center;
}
.img-container {
/* Attachments Section */
.attachments-section {
display: flex;
flex-direction: column;
gap: 10px;
margin: 6px 0 16px;
}
.attachments-header {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
color: var(--mat-sys-on-surface-variant, #64748b);
user-select: none;
}
.attachments-header-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-primary, #0284c7);
}
.attachments-badge {
font-size: 11px;
font-weight: 600;
padding: 1px 7px;
border-radius: 9999px;
background: var(--mat-sys-secondary-container, #e2e8f0);
color: var(--mat-sys-on-secondary-container, #1e293b);
}
.attachments-grid {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
}
.attachment-card {
position: relative;
width: 110px;
height: 155px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
background: var(--mat-sys-surface, #ffffff);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.img-container .overlay {
display: none;
.attachment-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.img-container:hover > .overlay {
display: inline-block;
.thumbnail-wrapper {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
outline: none;
}
.thumbnail-wrapper:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: -2px;
}
.thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.25s ease;
}
.thumbnail-wrapper:hover .thumbnail-img {
transform: scale(1.05);
}
.zoom-overlay {
position: absolute;
left: 60px;
top: 0;
inset: 0;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.thumbnail-wrapper:hover .zoom-overlay,
.thumbnail-wrapper:focus-visible .zoom-overlay {
opacity: 1;
}
.zoom-icon {
color: #ffffff;
font-size: 28px;
width: 28px;
height: 28px;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.5));
}
.attachment-card .delete-btn {
position: absolute;
top: 6px;
right: 6px;
width: 32px;
height: 32px;
min-width: 32px;
padding: 0;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
color: #dc2626;
opacity: 0;
transition:
opacity 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
transform 0.15s ease;
z-index: 3;
display: inline-flex;
align-items: center;
justify-content: center;
}
.attachment-card:hover .delete-btn,
.attachment-card:focus-within .delete-btn {
opacity: 1;
}
.attachment-card .delete-btn:hover,
.attachment-card .delete-btn:focus-visible {
background: #dc2626;
color: #ffffff;
transform: scale(1.08);
}
.attachment-card .delete-btn mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
line-height: 18px;
}
.upload-tile {
width: 110px;
height: 155px;
border-radius: 12px;
border: 2px dashed var(--mat-sys-outline, #cbd5e1);
background: var(--mat-sys-surface-container-low, #f8fafc);
cursor: pointer;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
transform 0.2s ease;
}
.upload-tile:hover {
border-color: var(--mat-sys-primary, #0284c7);
background: rgba(2, 132, 199, 0.05);
transform: translateY(-2px);
}
.upload-tile:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: 2px;
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 8px;
gap: 6px;
}
.upload-icon-circle {
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--mat-sys-primary-container, #e0f2fe);
color: var(--mat-sys-on-primary-container, #0284c7);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.upload-tile:hover .upload-icon-circle {
transform: scale(1.08);
}
.upload-icon-circle mat-icon {
font-size: 22px;
width: 22px;
height: 22px;
line-height: 22px;
}
.upload-title {
font-size: 12px;
font-weight: 600;
color: var(--mat-sys-on-surface, #1e293b);
}
.upload-subtitle {
font-size: 10px;
color: var(--mat-sys-on-surface-variant, #64748b);
}
.file-input-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.gold {
@@ -184,29 +184,70 @@
</mat-autocomplete>
</mat-form-field>
</div>
<div class="row-container">
@for (item of form.files().value(); track item) {
<div class="img-container" class="flex-auto basis-1-5">
<img
[src]="item.thumbnail"
alt="img"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
/>
<button mat-icon-button class="overlay" (click)="deleteImage(item)">
<mat-icon>delete</mat-icon>
</button>
</div>
}
<div class="img-container" class="flex flex-auto basis-1-5">
<label ng-href for="fileUp">
<img
alt="add"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTUwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjE1MCIgZmlsbD0iI2VlZSIvPjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjUwIiB5PSI3NSIgc3R5bGU9ImZpbGw6I2FhYTtmb250LXdlaWdodDpib2xkO2ZvbnQtc2l6ZToxMDBweDtmb250LWZhbWlseTpBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtkb21pbmFudC1iYXNlbGluZTpjZW50cmFsIj4rPC90ZXh0Pjwvc3ZnPg=="
/>
<div class="attachments-section">
<div class="attachments-header">
<mat-icon class="attachments-header-icon">attach_file</mat-icon>
<span class="attachments-title">Attachments</span>
@if (form.files().value().length > 0) {
<span class="attachments-badge">{{ form.files().value().length }}</span>
}
</div>
<div class="attachments-grid">
@for (item of form.files().value(); track item) {
<div class="attachment-card">
<div
class="thumbnail-wrapper"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
role="button"
aria-label="View enlarged attachment"
title="Click to preview"
>
<img [src]="item.thumbnail" alt="Attachment thumbnail" class="thumbnail-img" />
<div class="zoom-overlay">
<mat-icon class="zoom-icon">zoom_in</mat-icon>
</div>
</div>
<button
mat-icon-button
type="button"
class="delete-btn"
(click)="deleteImage(item); $event.stopPropagation()"
aria-label="Delete attachment"
title="Delete attachment"
>
<mat-icon>delete</mat-icon>
</button>
</div>
}
<label
for="fileUp"
class="upload-tile"
tabindex="0"
(keydown.enter)="fileInput.click()"
role="button"
aria-label="Upload image attachments"
>
<div class="upload-content">
<div class="upload-icon-circle">
<mat-icon>add_photo_alternate</mat-icon>
</div>
<span class="upload-title">Add Image</span>
<span class="upload-subtitle">Click to upload</span>
</div>
</label>
<input type="file" id="fileUp" multiple accept="image/*" style="display: none" (change)="detectFiles($event)" />
<input
#fileInput
type="file"
id="fileUp"
multiple
accept="image/*"
class="file-input-hidden"
(change)="detectFiles($event)"
/>
</div>
</div>
}
+230 -7
View File
@@ -16,19 +16,242 @@
justify-content: center;
}
.img-container {
/* Attachments Section */
.attachments-section {
display: flex;
flex-direction: column;
gap: 10px;
margin: 6px 0 16px;
}
.attachments-header {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
color: var(--mat-sys-on-surface-variant, #64748b);
user-select: none;
}
.attachments-header-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-primary, #0284c7);
}
.attachments-badge {
font-size: 11px;
font-weight: 600;
padding: 1px 7px;
border-radius: 9999px;
background: var(--mat-sys-secondary-container, #e2e8f0);
color: var(--mat-sys-on-secondary-container, #1e293b);
}
.attachments-grid {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
}
.attachment-card {
position: relative;
width: 110px;
height: 155px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
background: var(--mat-sys-surface, #ffffff);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.img-container .overlay {
display: none;
.attachment-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.img-container:hover > .overlay {
display: inline-block;
.thumbnail-wrapper {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
outline: none;
}
.thumbnail-wrapper:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: -2px;
}
.thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.25s ease;
}
.thumbnail-wrapper:hover .thumbnail-img {
transform: scale(1.05);
}
.zoom-overlay {
position: absolute;
left: 60px;
top: 0;
inset: 0;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.thumbnail-wrapper:hover .zoom-overlay,
.thumbnail-wrapper:focus-visible .zoom-overlay {
opacity: 1;
}
.zoom-icon {
color: #ffffff;
font-size: 28px;
width: 28px;
height: 28px;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.5));
}
.attachment-card .delete-btn {
position: absolute;
top: 6px;
right: 6px;
width: 32px;
height: 32px;
min-width: 32px;
padding: 0;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
color: #dc2626;
opacity: 0;
transition:
opacity 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
transform 0.15s ease;
z-index: 3;
display: inline-flex;
align-items: center;
justify-content: center;
}
.attachment-card:hover .delete-btn,
.attachment-card:focus-within .delete-btn {
opacity: 1;
}
.attachment-card .delete-btn:hover,
.attachment-card .delete-btn:focus-visible {
background: #dc2626;
color: #ffffff;
transform: scale(1.08);
}
.attachment-card .delete-btn mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
line-height: 18px;
}
.upload-tile {
width: 110px;
height: 155px;
border-radius: 12px;
border: 2px dashed var(--mat-sys-outline, #cbd5e1);
background: var(--mat-sys-surface-container-low, #f8fafc);
cursor: pointer;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
transform 0.2s ease;
}
.upload-tile:hover {
border-color: var(--mat-sys-primary, #0284c7);
background: rgba(2, 132, 199, 0.05);
transform: translateY(-2px);
}
.upload-tile:focus-visible {
outline: 2px solid var(--mat-sys-primary, #0284c7);
outline-offset: 2px;
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 8px;
gap: 6px;
}
.upload-icon-circle {
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--mat-sys-primary-container, #e0f2fe);
color: var(--mat-sys-on-primary-container, #0284c7);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.upload-tile:hover .upload-icon-circle {
transform: scale(1.08);
}
.upload-icon-circle mat-icon {
font-size: 22px;
width: 22px;
height: 22px;
line-height: 22px;
}
.upload-title {
font-size: 12px;
font-weight: 600;
color: var(--mat-sys-on-surface, #1e293b);
}
.upload-subtitle {
font-size: 10px;
color: var(--mat-sys-on-surface-variant, #64748b);
}
.file-input-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.gold {
+63 -22
View File
@@ -134,29 +134,70 @@
</mat-autocomplete>
</mat-form-field>
</div>
<div class="row-container">
@for (item of form.files().value(); track item) {
<div class="img-container" class="flex-auto basis-1-5">
<img
[src]="item.thumbnail"
alt="img"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
/>
<button mat-icon-button class="overlay" (click)="deleteImage(item)">
<mat-icon>delete</mat-icon>
</button>
</div>
}
<div class="img-container" class="flex flex-auto basis-1-5">
<label ng-href for="fileUp">
<img
alt="add"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTUwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjE1MCIgZmlsbD0iI2VlZSIvPjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjUwIiB5PSI3NSIgc3R5bGU9ImZpbGw6I2FhYTtmb250LXdlaWdodDpib2xkO2ZvbnQtc2l6ZToxMDBweDtmb250LWZhbWlseTpBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtkb21pbmFudC1iYXNlbGluZTpjZW50cmFsIj4rPC90ZXh0Pjwvc3ZnPg=="
/>
<div class="attachments-section">
<div class="attachments-header">
<mat-icon class="attachments-header-icon">attach_file</mat-icon>
<span class="attachments-title">Attachments</span>
@if (form.files().value().length > 0) {
<span class="attachments-badge">{{ form.files().value().length }}</span>
}
</div>
<div class="attachments-grid">
@for (item of form.files().value(); track item) {
<div class="attachment-card">
<div
class="thumbnail-wrapper"
(click)="zoomImage(item)"
(keyup.enter)="zoomImage(item)"
tabindex="0"
role="button"
aria-label="View enlarged attachment"
title="Click to preview"
>
<img [src]="item.thumbnail" alt="Attachment thumbnail" class="thumbnail-img" />
<div class="zoom-overlay">
<mat-icon class="zoom-icon">zoom_in</mat-icon>
</div>
</div>
<button
mat-icon-button
type="button"
class="delete-btn"
(click)="deleteImage(item); $event.stopPropagation()"
aria-label="Delete attachment"
title="Delete attachment"
>
<mat-icon>delete</mat-icon>
</button>
</div>
}
<label
for="fileUp"
class="upload-tile"
tabindex="0"
(keydown.enter)="fileInput.click()"
role="button"
aria-label="Upload image attachments"
>
<div class="upload-content">
<div class="upload-icon-circle">
<mat-icon>add_photo_alternate</mat-icon>
</div>
<span class="upload-title">Add Image</span>
<span class="upload-subtitle">Click to upload</span>
</div>
</label>
<input type="file" id="fileUp" multiple accept="image/*" style="display: none" (change)="detectFiles($event)" />
<input
#fileInput
type="file"
id="fileUp"
multiple
accept="image/*"
class="file-input-hidden"
(change)="detectFiles($event)"
/>
</div>
</div>
}
@@ -1 +1 @@
<img [src]="data" alt="image" (click)="close()" (keyup.enter)="close()" (keyup.space)="close()" tabindex="0" />
<img [src]="data.resized" alt="image" (click)="close()" (keyup.enter)="close()" (keyup.space)="close()" tabindex="0" />