feat: orgao field in contract modal uses organ dropdown

This commit is contained in:
Junior
2026-03-15 00:45:40 -03:00
parent 9ee07f5a28
commit 7c1ca6d061

View File

@@ -1,34 +1,503 @@
<!-- front-end/app/pages/gestao/contratos.vue --> <!-- front-end/app/pages/gestao/contratos.vue -->
<script setup lang="ts"> <script setup lang="ts">
import { contratos } from '~/data/mock/contratos' const { apiFetch } = useApi()
const { public: { apiBase } } = useRuntimeConfig()
const token = useCookie<string | null>('auth_token')
const columns = [ interface ApiContract {
{ id: 'numero', accessorKey: 'numero', header: 'Nº Contrato' }, ID: string
{ id: 'orgao', accessorKey: 'orgao', header: 'Órgão' }, Numero: string
{ id: 'valor', accessorKey: 'valor', header: 'Valor' }, Orgao: string
{ id: 'dataInicio', accessorKey: 'dataInicio', header: 'Início' }, Objeto: string
{ id: 'dataFim', accessorKey: 'dataFim', header: 'Término' }, Valor: number
{ id: 'fiscalContrato', accessorKey: 'fiscalContrato', header: 'Fiscal' }, DataInicio: string
{ id: 'sla', accessorKey: 'sla', header: 'SLA' }, DataFim: string
] FiscalContrato: string
Reajuste: string
ProrrogacaoMaxima: string | null
Sla: string
Status: string
}
interface ApiFile {
ID: string
Nome: string
Size: number
CreatedAt: string
}
const { data: contratos, refresh } = await useAsyncData('contratos', () =>
apiFetch<ApiContract[]>('/contracts')
)
interface ApiOrgan { ID: string; Nome: string }
const { data: orgaos } = await useAsyncData('orgaos-select', () =>
apiFetch<ApiOrgan[]>('/organs')
)
function formatDate(d: string) {
if (!d) return ''
return new Date(d).toLocaleDateString('pt-BR')
}
function formatValor(v: number) {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 }).format(v)
}
function formatFileSize(bytes: number) {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
// --- Menu dropdown ---
const openMenuId = ref<string | null>(null)
const menuPos = ref({ top: 0, right: 0 })
function toggleMenu(id: string, event: MouseEvent) {
if (openMenuId.value === id) {
openMenuId.value = null
return
}
const btn = event.currentTarget as HTMLElement
const rect = btn.getBoundingClientRect()
menuPos.value = {
top: rect.bottom + window.scrollY + 4,
right: window.innerWidth - rect.right,
}
openMenuId.value = id
}
const closeMenu = () => { openMenuId.value = null }
onMounted(() => document.addEventListener('click', closeMenu))
onUnmounted(() => document.removeEventListener('click', closeMenu))
// --- Modal Criar ---
const showCreate = ref(false)
const createForm = reactive({
numero: '', orgao: '', objeto: '', valor: 0,
data_inicio: '', data_fim: '', fiscal_contrato: '',
reajuste: '', prorrogacao_maxima: '', sla: '', status: 'ativo',
})
const createError = ref('')
const createLoading = ref(false)
async function criarContrato() {
createError.value = ''
createLoading.value = true
try {
await apiFetch('/contracts', { method: 'POST', body: { ...createForm } })
showCreate.value = false
Object.assign(createForm, { numero: '', orgao: '', objeto: '', valor: 0, data_inicio: '', data_fim: '', fiscal_contrato: '', reajuste: '', prorrogacao_maxima: '', sla: '', status: 'ativo' })
await refresh()
} catch (err: any) {
createError.value = err?.data?.error || 'Erro ao criar contrato.'
} finally {
createLoading.value = false
}
}
// --- Modal Editar ---
const showEdit = ref(false)
const editContract = ref<ApiContract | null>(null)
const editForm = reactive({
numero: '', orgao: '', objeto: '', valor: 0,
data_inicio: '', data_fim: '', fiscal_contrato: '',
reajuste: '', prorrogacao_maxima: '', sla: '', status: 'ativo',
})
const editError = ref('')
const editLoading = ref(false)
function abrirEditar(c: ApiContract) {
editContract.value = c
Object.assign(editForm, {
numero: c.Numero, orgao: c.Orgao, objeto: c.Objeto, valor: c.Valor,
data_inicio: c.DataInicio ? c.DataInicio.slice(0, 10) : '',
data_fim: c.DataFim ? c.DataFim.slice(0, 10) : '',
fiscal_contrato: c.FiscalContrato, reajuste: c.Reajuste,
prorrogacao_maxima: c.ProrrogacaoMaxima ? c.ProrrogacaoMaxima.slice(0, 10) : '',
sla: c.Sla, status: c.Status,
})
editError.value = ''
showEdit.value = true
}
async function salvarEdicao() {
if (!editContract.value) return
editError.value = ''
editLoading.value = true
try {
await apiFetch(`/contracts/${editContract.value.ID}`, { method: 'PUT', body: { ...editForm } })
showEdit.value = false
await refresh()
} catch (err: any) {
editError.value = err?.data?.error || 'Erro ao salvar.'
} finally {
editLoading.value = false
}
}
// --- Modal Confirmação Exclusão ---
const showDeleteConfirm = ref(false)
const deleteTarget = ref<ApiContract | null>(null)
const deletingId = ref<string | null>(null)
function pedirExclusao(c: ApiContract) {
deleteTarget.value = c
showDeleteConfirm.value = true
}
async function confirmarExclusao() {
if (!deleteTarget.value) return
deletingId.value = deleteTarget.value.ID
try {
await apiFetch(`/contracts/${deleteTarget.value.ID}`, { method: 'DELETE' })
showDeleteConfirm.value = false
deleteTarget.value = null
await refresh()
} catch {}
deletingId.value = null
}
// --- Modal Visualizar ---
const showView = ref(false)
const viewContract = ref<ApiContract | null>(null)
const viewFiles = ref<ApiFile[]>([])
const viewFilesLoading = ref(false)
const uploadLoading = ref(false)
async function abrirVisualizar(c: ApiContract) {
viewContract.value = c
showView.value = true
viewFilesLoading.value = true
try {
viewFiles.value = await apiFetch<ApiFile[]>(`/contracts/${c.ID}/files`)
} catch { viewFiles.value = [] }
viewFilesLoading.value = false
}
async function uploadArquivos(event: Event) {
const input = event.target as HTMLInputElement
if (!input.files?.length || !viewContract.value) return
uploadLoading.value = true
const formData = new FormData()
for (const file of Array.from(input.files)) formData.append('file', file)
try {
await $fetch(`${apiBase}/contracts/${viewContract.value.ID}/files`, {
method: 'POST',
headers: { Authorization: `Bearer ${token.value}` },
body: formData,
})
viewFiles.value = await apiFetch<ApiFile[]>(`/contracts/${viewContract.value.ID}/files`)
} catch {}
uploadLoading.value = false
input.value = ''
}
async function excluirArquivo(fileId: string) {
if (!viewContract.value) return
try {
await apiFetch(`/contracts/${viewContract.value.ID}/files/${fileId}`, { method: 'DELETE' })
viewFiles.value = viewFiles.value.filter(f => f.ID !== fileId)
} catch {}
}
async function downloadArquivo(contractId: string, fileId: string, nome: string) {
try {
const blob = await $fetch<Blob>(`${apiBase}/contracts/${contractId}/files/${fileId}/download`, {
headers: { Authorization: `Bearer ${token.value}` },
responseType: 'blob',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = nome
a.click()
URL.revokeObjectURL(url)
} catch {}
}
</script> </script>
<template> <template>
<div class="page"> <div class="page">
<AppTopbar title="Contratos" breadcrumb="Gestão · Contratos"> <AppTopbar title="Contratos" breadcrumb="Gestão · Contratos">
<template #actions> <template #actions>
<UButton size="sm" class="btn-primary">+ Novo Contrato</UButton> <UButton size="sm" class="btn-primary" @click="showCreate = true">+ Novo Contrato</UButton>
</template> </template>
</AppTopbar> </AppTopbar>
<div class="content"> <div class="content">
<div class="card"> <div class="card">
<UTable :data="contratos" :columns="columns"> <table class="tbl">
<template #valor-cell="{ row }"> <thead>
{{ new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 }).format(row.original.valor) }} <tr>
</template> <th></th>
<template #dataInicio-cell="{ row }">{{ row.original.dataInicio.toLocaleDateString('pt-BR') }}</template> <th>Órgão</th>
<template #dataFim-cell="{ row }">{{ row.original.dataFim.toLocaleDateString('pt-BR') }}</template> <th>Objeto</th>
</UTable> <th>Valor</th>
<th>Vigência</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="c in (contratos ?? [])" :key="c.ID">
<td class="mono">{{ c.Numero }}</td>
<td>{{ c.Orgao }}</td>
<td class="objeto">{{ c.Objeto }}</td>
<td class="valor">{{ formatValor(c.Valor) }}</td>
<td class="datas">{{ formatDate(c.DataInicio) }} {{ formatDate(c.DataFim) }}</td>
<td>
<span :class="['badge', `badge-${c.Status}`]">{{ c.Status }}</span>
</td>
<td class="menu-cell" @click.stop>
<button class="menu-btn" @click.stop="toggleMenu(c.ID, $event)"></button>
</td>
</tr>
<tr v-if="!contratos?.length">
<td colspan="7" class="empty">Nenhum contrato encontrado.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Dropdown menu (teleportado para o body para escapar do overflow) -->
<Teleport to="body">
<div
v-if="openMenuId"
class="dropdown-fixed"
:style="{ top: menuPos.top + 'px', right: menuPos.right + 'px' }"
@click.stop
>
<button @click="abrirVisualizar((contratos ?? []).find(c => c.ID === openMenuId)!); openMenuId = null">Visualizar</button>
<button @click="abrirEditar((contratos ?? []).find(c => c.ID === openMenuId)!); openMenuId = null">Editar</button>
<button class="drop-danger" @click="pedirExclusao((contratos ?? []).find(c => c.ID === openMenuId)!); openMenuId = null">Excluir</button>
</div>
</Teleport>
<!-- Modal Criar -->
<div v-if="showCreate" class="overlay">
<div class="modal">
<h2>Novo Contrato</h2>
<div class="fields-grid">
<div class="field">
<label>Número</label>
<UInput v-model="createForm.numero" placeholder="Ex: 001/2024" class="w-full" />
</div>
<div class="field">
<label>Órgão</label>
<select v-model="createForm.orgao" class="field-select">
<option value="" disabled>Selecione o órgão</option>
<option v-for="o in orgaos" :key="o.ID" :value="o.Nome">{{ o.Nome }}</option>
</select>
</div>
<div class="field field-full">
<label>Objeto</label>
<UInput v-model="createForm.objeto" placeholder="Descrição do objeto" class="w-full" />
</div>
<div class="field">
<label>Valor (R$)</label>
<UInput v-model.number="createForm.valor" type="number" placeholder="0" class="w-full" />
</div>
<div class="field">
<label>Status</label>
<select v-model="createForm.status" class="sel">
<option value="ativo">Ativo</option>
<option value="encerrado">Encerrado</option>
<option value="suspenso">Suspenso</option>
</select>
</div>
<div class="field">
<label>Data Início</label>
<UInput v-model="createForm.data_inicio" type="date" class="w-full" />
</div>
<div class="field">
<label>Data Fim</label>
<UInput v-model="createForm.data_fim" type="date" class="w-full" />
</div>
<div class="field">
<label>Fiscal</label>
<UInput v-model="createForm.fiscal_contrato" placeholder="Nome do fiscal" class="w-full" />
</div>
<div class="field">
<label>Reajuste</label>
<UInput v-model="createForm.reajuste" placeholder="Ex: IPCA" class="w-full" />
</div>
<div class="field">
<label>Prorrogação Máxima</label>
<UInput v-model="createForm.prorrogacao_maxima" type="date" class="w-full" />
</div>
<div class="field field-full">
<label>SLA</label>
<UInput v-model="createForm.sla" placeholder="Acordo de nível de serviço" class="w-full" />
</div>
</div>
<p v-if="createError" class="err">{{ createError }}</p>
<div class="modal-actions">
<button class="btn-cancel" @click="showCreate = false">Cancelar</button>
<UButton class="btn-primary" size="sm" :loading="createLoading" @click="criarContrato">Criar</UButton>
</div>
</div>
</div>
<!-- Modal Editar -->
<div v-if="showEdit" class="overlay">
<div class="modal">
<h2>Editar Contrato</h2>
<div class="fields-grid">
<div class="field">
<label>Número</label>
<UInput v-model="editForm.numero" class="w-full" />
</div>
<div class="field">
<label>Órgão</label>
<select v-model="editForm.orgao" class="field-select">
<option value="" disabled>Selecione o órgão</option>
<option v-for="o in orgaos" :key="o.ID" :value="o.Nome">{{ o.Nome }}</option>
</select>
</div>
<div class="field field-full">
<label>Objeto</label>
<UInput v-model="editForm.objeto" class="w-full" />
</div>
<div class="field">
<label>Valor (R$)</label>
<UInput v-model.number="editForm.valor" type="number" class="w-full" />
</div>
<div class="field">
<label>Status</label>
<select v-model="editForm.status" class="sel">
<option value="ativo">Ativo</option>
<option value="encerrado">Encerrado</option>
<option value="suspenso">Suspenso</option>
</select>
</div>
<div class="field">
<label>Data Início</label>
<UInput v-model="editForm.data_inicio" type="date" class="w-full" />
</div>
<div class="field">
<label>Data Fim</label>
<UInput v-model="editForm.data_fim" type="date" class="w-full" />
</div>
<div class="field">
<label>Fiscal</label>
<UInput v-model="editForm.fiscal_contrato" class="w-full" />
</div>
<div class="field">
<label>Reajuste</label>
<UInput v-model="editForm.reajuste" class="w-full" />
</div>
<div class="field">
<label>Prorrogação Máxima</label>
<UInput v-model="editForm.prorrogacao_maxima" type="date" class="w-full" />
</div>
<div class="field field-full">
<label>SLA</label>
<UInput v-model="editForm.sla" class="w-full" />
</div>
</div>
<p v-if="editError" class="err">{{ editError }}</p>
<div class="modal-actions">
<button class="btn-cancel" @click="showEdit = false">Cancelar</button>
<UButton class="btn-primary" size="sm" :loading="editLoading" @click="salvarEdicao">Salvar</UButton>
</div>
</div>
</div>
<!-- Modal Confirmação Exclusão -->
<div v-if="showDeleteConfirm" class="overlay">
<div class="modal modal-sm">
<h2>Excluir Contrato</h2>
<p class="confirm-msg">
Deseja realmente excluir o contrato <strong> {{ deleteTarget?.Numero }}</strong>
{{ deleteTarget?.Orgao }}?<br>
<span class="confirm-warn">Esta ação não pode ser desfeita.</span>
</p>
<div class="modal-actions">
<button class="btn-cancel" @click="showDeleteConfirm = false">Cancelar</button>
<button class="btn-danger" :disabled="deletingId !== null" @click="confirmarExclusao">
{{ deletingId ? 'Excluindo...' : 'Excluir' }}
</button>
</div>
</div>
</div>
<!-- Modal Visualizar -->
<div v-if="showView" class="overlay" @click.self="showView = false">
<div class="modal modal-view">
<div class="modal-header">
<div>
<h2>Contrato {{ viewContract?.Numero }}</h2>
<span :class="['badge', `badge-${viewContract?.Status}`]">{{ viewContract?.Status }}</span>
</div>
<button class="close-btn" @click="showView = false"></button>
</div>
<div class="view-grid">
<div class="view-field">
<label>Órgão</label>
<span>{{ viewContract?.Orgao }}</span>
</div>
<div class="view-field">
<label>Fiscal</label>
<span>{{ viewContract?.FiscalContrato || '—' }}</span>
</div>
<div class="view-field view-full">
<label>Objeto</label>
<span>{{ viewContract?.Objeto || '—' }}</span>
</div>
<div class="view-field">
<label>Valor</label>
<span>{{ formatValor(viewContract?.Valor ?? 0) }}</span>
</div>
<div class="view-field">
<label>Reajuste</label>
<span>{{ viewContract?.Reajuste || '—' }}</span>
</div>
<div class="view-field">
<label>Início da Vigência</label>
<span>{{ formatDate(viewContract?.DataInicio ?? '') }}</span>
</div>
<div class="view-field">
<label>Término da Vigência</label>
<span>{{ formatDate(viewContract?.DataFim ?? '') }}</span>
</div>
<div class="view-field">
<label>Prorrogação Máxima</label>
<span>{{ viewContract?.ProrrogacaoMaxima ? formatDate(viewContract.ProrrogacaoMaxima) : '—' }}</span>
</div>
<div class="view-field view-full">
<label>SLA</label>
<span>{{ viewContract?.Sla || '—' }}</span>
</div>
</div>
<div class="files-section">
<div class="files-header">
<h3>Arquivos</h3>
<label class="upload-btn" :class="{ loading: uploadLoading }">
<input type="file" multiple @change="uploadArquivos" style="display:none" :disabled="uploadLoading" />
{{ uploadLoading ? 'Enviando...' : '+ Anexar' }}
</label>
</div>
<div v-if="viewFilesLoading" class="files-empty">Carregando arquivos...</div>
<div v-else-if="viewFiles.length === 0" class="files-empty">Nenhum arquivo anexado.</div>
<div v-else class="files-list">
<div v-for="f in viewFiles" :key="f.ID" class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">{{ f.Nome }}</span>
<span class="file-size">{{ formatFileSize(f.Size) }}</span>
<button class="file-act" title="Download" @click="downloadArquivo(viewContract!.ID, f.ID, f.Nome)"></button>
<button class="file-act file-del" title="Remover" @click="excluirArquivo(f.ID)"></button>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-cancel" @click="showView = false">Fechar</button>
<UButton class="btn-primary" size="sm" @click="showView = false; abrirEditar(viewContract!)">Editar</UButton>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -37,6 +506,114 @@ const columns = [
<style scoped> <style scoped>
.page { display: flex; flex-direction: column; height: 100vh; } .page { display: flex; flex-direction: column; height: 100vh; }
.content { padding: 20px 22px; flex: 1; overflow-y: auto; } .content { padding: 20px 22px; flex: 1; overflow-y: auto; }
.card { background: white; border-radius: 11px; border: 1px solid #e2e8f0; overflow: hidden; } .card { background: white; border-radius: 11px; border: 1px solid #e2e8f0; overflow: visible; }
.btn-primary { background: linear-gradient(135deg, #667eea, #764ba2) !important; } .btn-primary { background: linear-gradient(135deg, #667eea, #764ba2) !important; }
.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
.tbl thead tr { background: #f8fafc; border-bottom: 1px solid #e2e8f0; }
.tbl th { padding: 10px 14px; text-align: left; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
.tbl td { padding: 12px 14px; border-bottom: 1px solid #f1f5f9; color: #1e293b; }
.tbl tbody tr:last-child td { border-bottom: none; }
.tbl tbody tr:hover { background: #fafafa; }
.mono { font-family: monospace; font-size: 12px; }
.objeto { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #64748b; }
.valor { font-variant-numeric: tabular-nums; }
.datas { font-size: 12px; color: #64748b; white-space: nowrap; }
.empty { text-align: center; color: #94a3b8; padding: 32px !important; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; text-transform: capitalize; }
.badge-ativo { background: #dcfce7; color: #16a34a; }
.badge-encerrado { background: #f1f5f9; color: #94a3b8; }
.badge-suspenso { background: #fff7ed; color: #ea580c; }
/* Menu dropdown */
.menu-cell { width: 40px; }
.menu-btn { background: none; border: none; font-size: 20px; cursor: pointer; padding: 2px 8px; border-radius: 6px; color: #94a3b8; line-height: 1; letter-spacing: 2px; }
.menu-btn:hover { background: #f1f5f9; color: #475569; }
.drop-danger { color: #dc2626 !important; }
.drop-danger:hover { background: #fff1f1 !important; }
/* Modal base */
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal { background: white; border-radius: 14px; padding: 28px; width: 100%; max-width: 560px; max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 50px rgba(0,0,0,0.2); }
.modal h2 { font-size: 16px; font-weight: 700; color: #0f172a; margin-bottom: 18px; }
.fields-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.field { display: flex; flex-direction: column; }
.field-full { grid-column: 1 / -1; }
.field label { font-size: 12px; font-weight: 600; color: #475569; margin-bottom: 5px; }
.sel { width: 100%; padding: 8px 10px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 13px; color: #0f172a; background: white; }
.err { color: #dc2626; font-size: 12px; margin-top: 8px; margin-bottom: 4px; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.btn-cancel { font-size: 13px; padding: 6px 14px; border-radius: 8px; border: 1px solid #e2e8f0; background: white; cursor: pointer; color: #64748b; }
.btn-cancel:hover { background: #f8fafc; }
/* Modal confirmação */
.modal-sm { max-width: 420px; }
.confirm-msg { font-size: 14px; color: #334155; line-height: 1.6; margin-bottom: 20px; }
.confirm-warn { font-size: 12px; color: #dc2626; }
.btn-danger { font-size: 13px; padding: 6px 14px; border-radius: 8px; background: #dc2626; color: white; border: none; cursor: pointer; font-weight: 600; }
.btn-danger:hover { background: #b91c1c; }
.btn-danger:disabled { opacity: 0.6; cursor: not-allowed; }
/* Modal visualizar */
.modal-view { max-width: 680px; }
.modal-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
.modal-header h2 { font-size: 16px; font-weight: 700; color: #0f172a; margin: 0 0 6px; }
.close-btn { background: none; border: none; font-size: 16px; color: #94a3b8; cursor: pointer; padding: 4px 8px; border-radius: 6px; }
.close-btn:hover { background: #f1f5f9; color: #475569; }
.view-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 24px; }
.view-full { grid-column: 1 / -1; }
.view-field { display: flex; flex-direction: column; gap: 3px; }
.view-field label { font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
.view-field span { font-size: 13px; color: #1e293b; }
/* Arquivos */
.files-section { border-top: 1px solid #f1f5f9; padding-top: 18px; margin-bottom: 20px; }
.files-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.files-header h3 { font-size: 11px; font-weight: 700; color: #94a3b8; margin: 0; text-transform: uppercase; letter-spacing: 0.5px; }
.upload-btn { font-size: 12px; font-weight: 600; padding: 5px 12px; border-radius: 8px; border: 1px dashed #667eea; color: #667eea; cursor: pointer; transition: all 0.15s; }
.upload-btn:hover { background: #f0f3ff; }
.upload-btn.loading { opacity: 0.6; cursor: not-allowed; }
.files-empty { font-size: 13px; color: #94a3b8; text-align: center; padding: 16px 0; }
.files-list { display: flex; flex-direction: column; gap: 6px; }
.file-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; }
.file-icon { font-size: 16px; flex-shrink: 0; }
.file-name { flex: 1; font-size: 13px; color: #1e293b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-size { font-size: 11px; color: #94a3b8; white-space: nowrap; flex-shrink: 0; }
.file-act { font-size: 13px; padding: 3px 7px; background: none; border: none; cursor: pointer; color: #667eea; border-radius: 4px; text-decoration: none; flex-shrink: 0; }
.file-act:hover { background: #f0f3ff; }
.file-del { color: #dc2626; }
.file-del:hover { background: #fff1f1 !important; }
.field-select {
width: 100%; border: 1px solid #e2e8f0; border-radius: 8px; padding: 8px 11px;
font-size: 13px; color: #1e293b; background: white; outline: none; font-family: inherit;
}
.field-select:focus { border-color: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,.15); }
</style>
<style>
.dropdown-fixed {
position: fixed;
background: white;
border: 1px solid #e2e8f0;
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
min-width: 140px;
z-index: 9999;
overflow: hidden;
}
.dropdown-fixed button {
display: block;
width: 100%;
padding: 9px 14px;
text-align: left;
font-size: 13px;
background: none;
border: none;
cursor: pointer;
color: #1e293b;
}
.dropdown-fixed button:hover { background: #f8fafc; }
.dropdown-fixed .drop-danger { color: #dc2626; }
.dropdown-fixed .drop-danger:hover { background: #fff1f1; }
</style> </style>