@@ -0,0 +1,838 @@
<!DOCTYPE html>
< html lang = "es" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Calendario — AALN Donna< / title >
< link rel = "preconnect" href = "https://fonts.googleapis.com" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
< link href = "https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel = "stylesheet" >
< style >
: root {
--navy : #1B3A6B ; --navy-d : #112446 ; --navy-l : #2D5090 ;
--gold : #C9A84C ; --gold-l : #E8C96A ; --gold-d : #9E7C2E ;
--off : #F7F6F3 ;
--gray-100 : #EEEDE9 ; --gray-200 : #D6D4CC ; --gray-400 : #9B9A96 ;
--gray-700 : #4A4946 ; --gray-900 : #1A1916 ;
--red : #c0392b ; --orange : #e67e22 ; --green : #27ae60 ; --blue : #2980b9 ;
--c-esc : #1B3A6B ; --c-adm : #6c5b95 ; --c-corp : #9E7C2E ; --c-mig : #2980b9 ; --c-lit : #722f37 ;
--shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , .06 ) , 0 4 px 16 px rgba ( 0 , 0 , 0 , .06 ) ;
}
* , * :: before , * :: after { box-sizing : border-box ; margin : 0 ; padding : 0 ; }
body { font-family : 'DM Sans' , system-ui , sans-serif ; color : var ( - - gray -900 ) ; background : var ( - - off ) ; font-size : 13.5 px ; }
a { color : inherit ; text-decoration : none ; }
button { font-family : inherit ; cursor : pointer ; }
header { background : var ( - - navy ) ; border-bottom : 3 px solid var ( - - gold ) ; padding : 0 24 px ;
display : flex ; align-items : center ; justify-content : space-between ; height : 58 px ;
position : sticky ; top : 0 ; z-index : 200 ; }
. header-left { display : flex ; align-items : center ; gap : 14 px ; }
. btn-back { display : inline-flex ; align-items : center ; gap : 6 px ;
background : rgba ( 255 , 255 , 255 , .08 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , .18 ) ;
color : rgba ( 255 , 255 , 255 , .9 ) ; padding : 6 px 12 px 6 px 9 px ; border-radius : 8 px ;
font-size : 12 px ; transition : all .15 s ; white-space : nowrap ; }
. btn-back : hover { background : var ( - - gold ) ; border-color : var ( - - gold ) ; color : var ( - - navy ) ; }
. btn-back svg { width : 14 px ; height : 14 px ; }
. logo { display : flex ; align-items : center ; gap : 14 px ; }
. logo-mark { width : 32 px ; height : 32 px ; background : var ( - - gold ) ; border-radius : 6 px ;
display : flex ; align-items : center ; justify-content : center ;
font-family : 'DM Serif Display' , serif ; font-size : 15 px ; color : var ( - - navy ) ; }
. logo-text { display : flex ; flex-direction : column ; line-height : 1.2 ; }
. logo-firm { font-size : 11 px ; font-weight : 600 ; color : var ( - - gold ) ; letter-spacing : .08 em ; text-transform : uppercase ; }
. logo-tool { font-size : 13 px ; font-weight : 400 ; color : rgba ( 255 , 255 , 255 , .85 ) ; }
. header-right { display : flex ; align-items : center ; gap : 16 px ; }
. sync-status { display : flex ; align-items : center ; gap : 6 px ; font-size : 11 px ; color : rgba ( 255 , 255 , 255 , .55 ) ; }
. sync-dot { width : 7 px ; height : 7 px ; border-radius : 50 % ; background : #4CAF50 ; animation : pulse 2.5 s ease-in-out infinite ; }
. sync-dot . syncing { background : var ( - - gold ) ; }
. sync-dot . error { background : #ef5350 ; animation : none ; }
@ keyframes pulse { 0 % , 100 % { opacity : 1 } 50 % { opacity : .4 } }
. user-chip { display : flex ; align-items : center ; gap : 8 px ; background : rgba ( 255 , 255 , 255 , .1 ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , .15 ) ; border-radius : 20 px ; padding : 5 px 12 px 5 px 6 px ; }
. user-avatar { width : 24 px ; height : 24 px ; border-radius : 50 % ; background : var ( - - gold ) ; color : var ( - - navy ) ;
display : flex ; align-items : center ; justify-content : center ; font-size : 11 px ; font-weight : 700 ; }
. user-name { font-size : 12 px ; color : rgba ( 255 , 255 , 255 , .85 ) ; max-width : 160 px ; overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ; }
. btn-logout { font-size : 11 px ; padding : 4 px 10 px ; border : 1 px solid rgba ( 255 , 255 , 255 , .2 ) ; border-radius : 6 px ;
background : transparent ; color : rgba ( 255 , 255 , 255 , .7 ) ; transition : all .15 s ; }
. btn-logout : hover { background : rgba ( 255 , 255 , 255 , .1 ) ; color : #fff ; }
. cal-toolbar { background : #fff ; border-bottom : 1 px solid var ( - - gray -100 ) ; padding : 12 px 24 px ;
display : flex ; align-items : center ; gap : 14 px ; flex-wrap : wrap ; box-shadow : var ( - - shadow ) ; }
. view-tabs { display : flex ; background : var ( - - gray -100 ) ; border-radius : 8 px ; padding : 2 px ; }
. view-tabs button { padding : 6 px 14 px ; background : transparent ; border : none ; border-radius : 6 px ;
font-size : 12.5 px ; color : var ( - - gray -700 ) ; transition : all .15 s ; }
. view-tabs button : hover { background : rgba ( 0 , 0 , 0 , .04 ) ; }
. view-tabs button . active { background : var ( - - navy ) ; color : #fff ; }
. date-nav { display : flex ; align-items : center ; gap : 6 px ; }
. date-nav button { width : 30 px ; height : 30 px ; border : 1 px solid var ( - - gray -200 ) ; background : #fff ;
border-radius : 6 px ; color : var ( - - gray -700 ) ; font-size : 18 px ; transition : all .15 s ;
display : inline-flex ; align-items : center ; justify-content : center ; }
. date-nav button : hover { background : var ( - - off ) ; border-color : var ( - - navy - l ) ; color : var ( - - navy ) ; }
. date-nav . today-btn { width : auto ; padding : 0 12 px ; font-size : 12 px ; font-weight : 500 ; }
. period-label { font-family : 'DM Serif Display' , serif ; font-size : 18 px ; color : var ( - - navy ) ;
margin-left : 4 px ; min-width : 200 px ; text-transform : capitalize ; }
. search-box { flex : 1 ; min-width : 200 px ; max-width : 300 px ; position : relative ; }
. search-box input { width : 100 % ; padding : 7 px 10 px 7 px 30 px ; border : 1 px solid var ( - - gray -200 ) ;
border-radius : 8 px ; font-family : inherit ; font-size : 12.5 px ; background : var ( - - off ) ; }
. search-box input : focus { outline : none ; border-color : var ( - - navy - l ) ; background : #fff ;
box-shadow : 0 0 0 3 px rgba ( 27 , 58 , 107 , .08 ) ; }
. search-box svg { position : absolute ; left : 9 px ; top : 50 % ; transform : translateY ( -50 % ) ;
width : 14 px ; height : 14 px ; color : var ( - - gray -400 ) ; pointer-events : none ; }
. src-filters { display : flex ; gap : 6 px ; flex-wrap : wrap ; }
. chip { display : inline-flex ; align-items : center ; gap : 5 px ; padding : 4 px 10 px ; border-radius : 20 px ;
font-size : 11.5 px ; font-weight : 600 ; cursor : pointer ; user-select : none ; transition : all .15 s ;
border : 1.5 px solid transparent ; }
. chip . cdot { width : 8 px ; height : 8 px ; border-radius : 50 % ; }
. chip . on { background : #fff ; border-color : currentColor ; }
. chip . off { background : var ( - - gray -100 ) ; color : var ( - - gray -400 ) !important ; opacity : .65 ; }
. chip . off . cdot { background : var ( - - gray -400 ) !important ; }
. chip [ data-src = "esc" ] { color : var ( - - c - esc ) ; }
. chip [ data-src = "adm" ] { color : var ( - - c - adm ) ; }
. chip [ data-src = "corp" ] { color : var ( - - c - corp ) ; }
. chip [ data-src = "mig" ] { color : var ( - - c - mig ) ; }
. chip [ data-src = "lit" ] { color : var ( - - c - lit ) ; }
. chip [ data-src = "esc" ] . cdot { background : var ( - - c - esc ) ; }
. chip [ data-src = "adm" ] . cdot { background : var ( - - c - adm ) ; }
. chip [ data-src = "corp" ] . cdot { background : var ( - - c - corp ) ; }
. chip [ data-src = "mig" ] . cdot { background : var ( - - c - mig ) ; }
. chip [ data-src = "lit" ] . cdot { background : var ( - - c - lit ) ; }
. cal-kpis { display : grid ; grid-template-columns : repeat ( 4 , 1 fr ) ; gap : 10 px ; padding : 14 px 24 px 0 ; }
. cal-kpi { background : #fff ; border : 1 px solid var ( - - gray -100 ) ; border-radius : 10 px ; padding : 12 px 16 px ;
box-shadow : var ( - - shadow ) ; border-left : 4 px solid var ( - - navy ) ; }
. cal-kpi . warn { border-left-color : var ( - - orange ) ; }
. cal-kpi . danger { border-left-color : var ( - - red ) ; }
. cal-kpi-label { font-size : 10.5 px ; font-weight : 600 ; color : var ( - - gray -400 ) ; text-transform : uppercase ; letter-spacing : .06 em ; }
. cal-kpi-val { font-family : 'DM Serif Display' , serif ; font-size : 26 px ; color : var ( - - navy ) ; line-height : 1.1 ; margin-top : 2 px ; }
. cal-kpi . warn . cal-kpi-val { color : var ( - - orange ) ; }
. cal-kpi . danger . cal-kpi-val { color : var ( - - red ) ; }
@ media ( max-width : 720px ) { . cal-kpis { grid-template-columns : repeat ( 2 , 1 fr ) ; } }
. upcoming-strip { padding : 14 px 24 px 0 ; overflow-x : auto ; }
. upcoming-strip h4 { font-size : 11 px ; font-weight : 600 ; color : var ( - - gray -400 ) ; letter-spacing : .08 em ;
text-transform : uppercase ; margin-bottom : 8 px ; }
. upcoming-row { display : flex ; gap : 10 px ; min-width : fit-content ; padding-bottom : 4 px ; }
. up-card { flex : 0 0 230 px ; background : #fff ; border : 1 px solid var ( - - gray -100 ) ;
border-left : 4 px solid var ( - - orange ) ; border-radius : 8 px ; padding : 8 px 12 px ;
cursor : pointer ; transition : all .15 s ; box-shadow : var ( - - shadow ) ; }
. up-card . overdue { border-left-color : var ( - - red ) ; }
. up-card : hover { transform : translateY ( -1 px ) ; box-shadow : 0 4 px 16 px rgba ( 17 , 36 , 70 , .12 ) ; border-color : var ( - - navy - l ) ; }
. up-card-date { font-family : 'DM Serif Display' , serif ; font-size : 13 px ; color : var ( - - navy ) ; }
. up-card-rem { font-size : 10.5 px ; font-weight : 700 ; color : var ( - - orange ) ; letter-spacing : .05 em ; text-transform : uppercase ; margin-left : 6 px ; }
. up-card . overdue . up-card-rem { color : var ( - - red ) ; }
. up-card-title { font-size : 12 px ; color : var ( - - gray -700 ) ; margin-top : 4 px ; overflow : hidden ;
text-overflow : ellipsis ; white-space : nowrap ; }
. up-card-meta { font-size : 10.5 px ; color : var ( - - gray -400 ) ; margin-top : 2 px ; overflow : hidden ;
text-overflow : ellipsis ; white-space : nowrap ; }
. up-empty { padding : 8 px 14 px ; font-size : 12 px ; color : var ( - - gray -400 ) ; }
main { padding : 16 px 24 px 24 px ; }
. year-grid { display : grid ; grid-template-columns : repeat ( 4 , 1 fr ) ; gap : 14 px ; }
@ media ( max-width : 1100px ) { . year-grid { grid-template-columns : repeat ( 3 , 1 fr ) ; } }
@ media ( max-width : 780px ) { . year-grid { grid-template-columns : repeat ( 2 , 1 fr ) ; } }
@ media ( max-width : 480px ) { . year-grid { grid-template-columns : 1 fr ; } }
. year-mini { background : #fff ; border : 1 px solid var ( - - gray -100 ) ; border-radius : 10 px ; padding : 12 px ;
cursor : pointer ; transition : all .15 s ; box-shadow : var ( - - shadow ) ; }
. year-mini : hover { transform : translateY ( -2 px ) ; box-shadow : 0 6 px 20 px rgba ( 17 , 36 , 70 , .12 ) ; border-color : var ( - - navy - l ) ; }
. year-mini-title { font-family : 'DM Serif Display' , serif ; font-size : 15 px ; color : var ( - - navy ) ;
text-transform : capitalize ; margin-bottom : 8 px ; }
. year-mini-grid { display : grid ; grid-template-columns : repeat ( 7 , 1 fr ) ; gap : 2 px ; }
. year-mini-dow { font-size : 9 px ; color : var ( - - gray -400 ) ; text-align : center ; padding : 2 px 0 ;
font-weight : 600 ; text-transform : uppercase ; }
. year-mini-day { aspect-ratio : 1 ; display : flex ; align-items : center ; justify-content : center ;
font-size : 10 px ; color : var ( - - gray -700 ) ; border-radius : 3 px ; position : relative ; }
. year-mini-day . dim { color : var ( - - gray -200 ) ; }
. year-mini-day . today { background : var ( - - navy ) ; color : #fff ; font-weight : 700 ; }
. year-mini-day . dots { position : absolute ; bottom : 1 px ; left : 50 % ; transform : translateX ( -50 % ) ;
display : flex ; gap : 1 px ; }
. year-mini-day . dot { width : 3 px ; height : 3 px ; border-radius : 50 % ; }
. year-mini-day . has-deadline :: after {
content : '' ; position : absolute ; top : 1 px ; right : 1 px ; width : 5 px ; height : 5 px ; border-radius : 50 % ; background : var ( - - red ) ;
}
. month-grid { background : var ( - - gray -200 ) ; border : 1 px solid var ( - - gray -200 ) ; border-radius : 10 px ;
overflow : hidden ; box-shadow : var ( - - shadow ) ; display : grid ;
grid-template-columns : repeat ( 7 , 1 fr ) ; gap : 1 px ; }
. month-dow { background : var ( - - navy ) ; color : rgba ( 255 , 255 , 255 , .85 ) ; padding : 8 px ;
font-size : 11 px ; font-weight : 600 ; text-transform : uppercase ; letter-spacing : .06 em ; text-align : center ; }
. month-cell { background : #fff ; min-height : 118 px ; padding : 6 px ; cursor : pointer ;
transition : background .15 s ; display : flex ; flex-direction : column ; gap : 2 px ; overflow : hidden ; }
. month-cell : hover { background : #FAFBFD ; }
. month-cell . dim { background : #F6F5F0 ; }
. month-cell . dim . cell-num { color : var ( - - gray -400 ) ; }
. month-cell . today { background : #FFFBEB ; }
. month-cell . cell-num { font-size : 11.5 px ; font-weight : 600 ; color : var ( - - gray -700 ) ; margin-bottom : 2 px ; }
. month-cell . today . cell-num { display : inline-flex ; align-items : center ; justify-content : center ;
background : var ( - - navy ) ; color : #fff ; border-radius : 50 % ;
width : 22 px ; height : 22 px ; font-size : 11 px ; align-self : flex-start ; }
. evt-chip { font-size : 10.5 px ; padding : 2 px 6 px ; border-radius : 4 px ; color : #fff ; cursor : pointer ;
overflow : hidden ; white-space : nowrap ; text-overflow : ellipsis ; line-height : 1.3 ;
border-left : 3 px solid rgba ( 0 , 0 , 0 , .18 ) ; }
. evt-chip : hover { filter : brightness ( 1.1 ) ; }
. evt-chip . overdue { animation : flash 1.6 s ease-in-out infinite ; }
@ keyframes flash { 0 % , 100 % { filter : brightness ( 1 ) } 50 % { filter : brightness ( 1.15 ) } }
. evt-more { font-size : 10 px ; color : var ( - - gray -400 ) ; padding-left : 2 px ; }
. week-grid { background : var ( - - gray -200 ) ; border : 1 px solid var ( - - gray -200 ) ; border-radius : 10 px ;
overflow : hidden ; box-shadow : var ( - - shadow ) ; display : grid ;
grid-template-columns : repeat ( 7 , 1 fr ) ; gap : 1 px ; min-height : 520 px ; }
. week-col { background : #fff ; display : flex ; flex-direction : column ; }
. week-col-head { background : var ( - - navy ) ; color : #fff ; padding : 10 px 8 px ; text-align : center ; }
. week-col-head . today { background : var ( - - gold - d ) ; }
. week-col-day { font-size : 10.5 px ; text-transform : uppercase ; letter-spacing : .08 em ; opacity : .85 ; }
. week-col-num { font-family : 'DM Serif Display' , serif ; font-size : 22 px ; line-height : 1.1 ; margin-top : 2 px ; }
. week-col-body { padding : 8 px ; flex : 1 ; display : flex ; flex-direction : column ; gap : 6 px ; cursor : pointer ; }
. week-empty { font-size : 11 px ; color : var ( - - gray -400 ) ; padding : 6 px ; text-align : center ; }
. week-evt { padding : 7 px 10 px ; border-radius : 6 px ; color : #fff ; cursor : pointer ; font-size : 11 px ;
border-left : 4 px solid rgba ( 0 , 0 , 0 , .18 ) ; transition : transform .1 s ; }
. week-evt : hover { transform : translateY ( -1 px ) ; }
. week-evt . tt { font-size : 11 px ; font-weight : 600 ; line-height : 1.25 ; }
. week-evt . meta { font-size : 10 px ; opacity : .85 ; margin-top : 2 px ; }
. day-view { max-width : 780 px ; margin : 0 auto ; }
. day-header { font-family : 'DM Serif Display' , serif ; font-size : 30 px ; color : var ( - - navy ) ;
text-transform : capitalize ; margin-bottom : 6 px ; }
. day-sub { font-size : 12 px ; color : var ( - - gray -400 ) ; margin-bottom : 14 px ; }
. day-evt { display : grid ; grid-template-columns : 14 px 1 fr auto ; gap : 14 px ; background : #fff ;
padding : 14 px 16 px ; margin-bottom : 10 px ; border-radius : 10 px ; box-shadow : var ( - - shadow ) ;
cursor : pointer ; transition : transform .15 s ; }
. day-evt : hover { transform : translateX ( 3 px ) ; }
. day-evt-bar { background : var ( - - navy ) ; border-radius : 14 px ; }
. day-evt-title { font-family : 'DM Serif Display' , serif ; font-size : 17 px ; color : var ( - - navy ) ; line-height : 1.2 ; }
. day-evt-meta { font-size : 12 px ; color : var ( - - gray -700 ) ; margin-top : 4 px ; }
. day-evt-badges { display : flex ; flex-direction : column ; align-items : flex-end ; gap : 4 px ; }
. evt-tag { font-size : 10 px ; font-weight : 700 ; padding : 2 px 8 px ; border-radius : 20 px ;
letter-spacing : .05 em ; text-transform : uppercase ; }
. cal-empty { text-align : center ; padding : 40 px ; color : var ( - - gray -400 ) ; font-size : 13 px ;
background : #fff ; border-radius : 10 px ; border : 1 px dashed var ( - - gray -200 ) ; }
. modal-overlay { display : none ; position : fixed ; inset : 0 ; background : rgba ( 17 , 36 , 70 , .6 ) ;
z-index : 1000 ; align-items : center ; justify-content : center ; padding : 24 px ; }
. modal-overlay . open { display : flex ; }
. modal { background : #fff ; border-radius : 14 px ; width : 100 % ; max-width : 520 px ; max-height : 90 vh ;
overflow : hidden ; display : flex ; flex-direction : column ; box-shadow : 0 20 px 60 px rgba ( 0 , 0 , 0 , .3 ) ; }
. modal-head { background : var ( - - navy ) ; color : #fff ; padding : 16 px 22 px ; display : flex ;
justify-content : space-between ; align-items : center ; border-bottom : 2 px solid var ( - - gold ) ; }
. modal-head h3 { font-family : 'DM Serif Display' , serif ; font-size : 18 px ; font-weight : 400 ; margin : 0 ; max-width : 84 % ; }
. modal-head . close { background : transparent ; border : none ; color : rgba ( 255 , 255 , 255 , .7 ) ;
font-size : 22 px ; line-height : 1 ; }
. modal-head . close : hover { color : #fff ; }
. modal-body { padding : 18 px 22 px ; overflow-y : auto ; flex : 1 ; }
. modal-body . row { display : flex ; justify-content : space-between ; align-items : flex-start ; gap : 14 px ;
padding : 9 px 0 ; border-bottom : 1 px solid var ( - - gray -100 ) ; }
. modal-body . row : last-child { border-bottom : none ; }
. modal-body . row . k { font-size : 10.5 px ; font-weight : 700 ; color : var ( - - navy ) ;
text-transform : uppercase ; letter-spacing : .08 em ; min-width : 130 px ; flex-shrink : 0 ; }
. modal-body . row . v { font-size : 13 px ; color : var ( - - gray -900 ) ; text-align : right ; flex : 1 ; word-break : break-word ; }
. modal-body . src-tag { display : inline-block ; padding : 3 px 10 px ; border-radius : 20 px ; font-size : 10.5 px ;
font-weight : 700 ; letter-spacing : .05 em ; text-transform : uppercase ; color : #fff ; }
. modal-foot { padding : 14 px 22 px ; border-top : 1 px solid var ( - - gray -100 ) ; background : var ( - - off ) ;
display : flex ; justify-content : space-between ; gap : 10 px ; }
. btn-secondary { padding : 8 px 16 px ; background : transparent ; color : var ( - - gray -700 ) ;
border : 1 px solid var ( - - gray -200 ) ; border-radius : 8 px ; font-size : 13 px ; }
. btn-secondary : hover { background : var ( - - gray -100 ) ; }
. btn-primary { padding : 8 px 20 px ; background : var ( - - navy ) ; color : #fff ; border : none ;
border-radius : 8 px ; font-size : 13 px ; font-weight : 500 ; }
. btn-primary : hover { background : var ( - - navy - d ) ; }
. toast { position : fixed ; bottom : 24 px ; right : 24 px ; background : var ( - - navy - d ) ; color : #fff ;
padding : 11 px 18 px ; border-radius : 8 px ; font-size : 12.5 px ; border-left : 3 px solid var ( - - gold ) ;
box-shadow : 0 4 px 20 px rgba ( 0 , 0 , 0 , .2 ) ; transform : translateY ( 20 px ) ; opacity : 0 ;
transition : all .25 s ; z-index : 2000 ; }
. toast . show { transform : translateY ( 0 ) ; opacity : 1 ; }
. toast . error { border-left-color : #ef5350 ; }
footer . legend { text-align : center ; padding : 14 px 24 px 24 px ; font-size : 11 px ; color : var ( - - gray -400 ) ; }
footer . legend . pill { display : inline-block ; padding : 2 px 8 px ; border-radius : 20 px ; color : #fff ;
font-weight : 600 ; margin : 0 2 px ; font-size : 10.5 px ; }
< / style >
< / head >
< body >
< header >
< div class = "header-left" >
< a class = "btn-back" href = "/" title = "Volver al portal Donna" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.4" stroke-linecap = "round" stroke-linejoin = "round" > < polyline points = "15 18 9 12 15 6" / > < / svg >
Portal
< / a >
< div class = "logo" >
< div class = "logo-mark" > A< / div >
< div class = "logo-text" >
< span class = "logo-firm" > AALN Law< / span >
< span class = "logo-tool" > Calendario< / span >
< / div >
< / div >
< / div >
< div class = "header-right" >
< div class = "sync-status" >
< div class = "sync-dot" id = "sync-dot" > < / div >
< span id = "sync-label" > Conectando…< / span >
< / div >
< div class = "user-chip" >
< div class = "user-avatar" id = "user-avatar" > ?< / div >
< span class = "user-name" id = "user-name" > …< / span >
< / div >
< a class = "btn-logout" href = "/auth/logout.php" onclick = "return confirm('¿Cerrar sesión?');" > Cerrar sesión< / a >
< / div >
< / header >
< div class = "cal-toolbar" >
< div class = "view-tabs" id = "view-tabs" >
< button data-view = "year" > Año< / button >
< button data-view = "month" class = "active" > Mes< / button >
< button data-view = "week" > Semana< / button >
< button data-view = "day" > Día< / button >
< / div >
< div class = "date-nav" >
< button id = "nav-prev" title = "Anterior (←)" > ‹ < / button >
< button class = "today-btn" id = "nav-today" title = "Hoy (T)" > Hoy< / button >
< button id = "nav-next" title = "Siguiente (→)" > › < / button >
< / div >
< span class = "period-label" id = "period-label" > —< / span >
< div class = "search-box" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "11" cy = "11" r = "8" / > < path d = "m21 21-4.35-4.35" / > < / svg >
< input type = "search" id = "search" placeholder = "Buscar evento, cliente o abogado…" >
< / div >
< div class = "src-filters" id = "src-filters" >
< span class = "chip on" data-src = "esc" > < span class = "cdot" > < / span > Escrituras< / span >
< span class = "chip on" data-src = "adm" > < span class = "cdot" > < / span > Admin< / span >
< span class = "chip on" data-src = "corp" > < span class = "cdot" > < / span > Corporativo< / span >
< span class = "chip on" data-src = "mig" > < span class = "cdot" > < / span > Migración< / span >
< span class = "chip on" data-src = "lit" > < span class = "cdot" > < / span > Litigios< / span >
< / div >
< / div >
< div class = "cal-kpis" >
< div class = "cal-kpi" > < div class = "cal-kpi-label" > Total eventos< / div > < div class = "cal-kpi-val" id = "k-total" > —< / div > < / div >
< div class = "cal-kpi warn" > < div class = "cal-kpi-label" > Vencen ≤ 7 días< / div > < div class = "cal-kpi-val" id = "k-7d" > —< / div > < / div >
< div class = "cal-kpi warn" > < div class = "cal-kpi-label" > Vencen ≤ 30 días< / div > < div class = "cal-kpi-val" id = "k-30d" > —< / div > < / div >
< div class = "cal-kpi danger" > < div class = "cal-kpi-label" > Vencidos abiertos< / div > < div class = "cal-kpi-val" id = "k-overdue" > —< / div > < / div >
< / div >
< div class = "upcoming-strip" >
< h4 > Próximos vencimientos< / h4 >
< div class = "upcoming-row" id = "upcoming-row" > < div class = "up-empty" > Cargando…< / div > < / div >
< / div >
< main id = "cal-main" > < / main >
< footer class = "legend" >
< span class = "pill" style = "background:var(--c-esc)" > Escrituras< / span >
< span class = "pill" style = "background:var(--c-adm)" > Admin< / span >
< span class = "pill" style = "background:var(--c-corp)" > Corp< / span >
< span class = "pill" style = "background:var(--c-mig)" > Migración< / span >
< span class = "pill" style = "background:var(--c-lit)" > Litigios< / span >
< span class = "pill" style = "background:var(--orange)" > ≤7d< / span >
< span class = "pill" style = "background:var(--red)" > Vencido< / span >
· Atajos: < b > ← →< / b > mueve período · < b > T< / b > hoy · < b > Y/M/W/D< / b > cambia vista · < b > Esc< / b > cierra
< / footer >
< div class = "modal-overlay" id = "evt-modal" onclick = "if(event.target.id==='evt-modal')closeEvent()" >
< div class = "modal" >
< header class = "modal-head" >
< h3 id = "evt-title" > Detalle del evento< / h3 >
< button class = "close" onclick = "closeEvent()" title = "Cerrar (Esc)" > ✕< / button >
< / header >
< div class = "modal-body" id = "evt-body" > < / div >
< div class = "modal-foot" >
< button class = "btn-secondary" onclick = "closeEvent()" > ← Regresar al calendario< / button >
< button class = "btn-primary" id = "btn-edit" > Editar evento< / button >
< / div >
< / div >
< / div >
< div class = "toast" id = "toast" > < / div >
< script >
const DEPT _LABELS = { adm : 'Administrativo' , corp : 'Corporativo' , mig : 'Migración' , lit : 'Litigios' } ;
const SRC _LABELS = { esc : 'Escrituras' , adm : 'Trámites Admin' , corp : 'Trámites Corp' , mig : 'Trámites Mig' , lit : 'Trámites Lit' } ;
const SRC _COLORS = { esc : '#1B3A6B' , adm : '#6c5b95' , corp : '#9E7C2E' , mig : '#2980b9' , lit : '#722f37' } ;
const COLOR _OVERDUE = '#c0392b' ;
const COLOR _UPCOMING = '#e67e22' ;
const MES _ES = [ 'enero' , 'febrero' , 'marzo' , 'abril' , 'mayo' , 'junio' , 'julio' , 'agosto' , 'septiembre' , 'octubre' , 'noviembre' , 'diciembre' ] ;
const DOW _FULL = [ 'lunes' , 'martes' , 'miércoles' , 'jueves' , 'viernes' , 'sábado' , 'domingo' ] ;
const DOW _SHORT = [ 'Lun' , 'Mar' , 'Mié' , 'Jue' , 'Vie' , 'Sáb' , 'Dom' ] ;
const CLOSED _STATUS = new Set ( [ 'Culminado' , 'Facturado' , 'Inscrito' ] ) ;
let events = [ ] ;
let view = 'month' ;
let cursor = stripTime ( new Date ( ) ) ;
let filters = new Set ( [ 'esc' , 'adm' , 'corp' , 'mig' , 'lit' ] ) ;
let search = '' ;
let selected = null ;
let authUser = { email : '' , name : '' } ;
function $ ( id ) { return document . getElementById ( id ) ; }
function esc ( s ) { return ( s == null ? '' : String ( s ) ) . replace ( /&/g , '&' ) . replace ( /"/g , '"' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) ; }
function stripTime ( d ) { const o = new Date ( d ) ; o . setHours ( 0 , 0 , 0 , 0 ) ; return o ; }
function ymd ( d ) {
if ( ! d ) return '' ;
if ( typeof d === 'string' ) {
if ( /^\d{4}-\d{2}-\d{2}/ . test ( d ) ) return d . slice ( 0 , 10 ) ;
d = new Date ( d ) ;
}
if ( isNaN ( d ) ) return '' ;
const m = String ( d . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
const day = String ( d . getDate ( ) ) . padStart ( 2 , '0' ) ;
return ` ${ d . getFullYear ( ) } - ${ m } - ${ day } ` ;
}
function parseYMD ( s ) {
if ( ! s ) return null ;
const [ y , m , d ] = s . split ( '-' ) . map ( Number ) ;
if ( ! y ) return null ;
return new Date ( y , ( m || 1 ) - 1 , d || 1 ) ;
}
function addDays ( d , n ) { const o = new Date ( d ) ; o . setDate ( o . getDate ( ) + n ) ; return o ; }
function addMonths ( d , n ) { return new Date ( d . getFullYear ( ) , d . getMonth ( ) + n , 1 ) ; }
function startOfMonth ( d ) { return new Date ( d . getFullYear ( ) , d . getMonth ( ) , 1 ) ; }
function startOfWeek ( d ) { const day = ( d . getDay ( ) + 6 ) % 7 ; return addDays ( d , - day ) ; }
function sameDate ( a , b ) { return ymd ( a ) === ymd ( b ) ; }
function daysToToday ( s ) {
const today = stripTime ( new Date ( ) ) ;
const date = parseYMD ( s ) ;
if ( ! date ) return null ;
return Math . round ( ( date - today ) / ( 1000 * 60 * 60 * 24 ) ) ;
}
function setSync ( state , label ) {
const dot = $ ( 'sync-dot' ) ; if ( ! dot ) return ;
dot . className = 'sync-dot' + ( state !== 'ok' ? ' ' + state : '' ) ;
$ ( 'sync-label' ) . textContent = label ;
}
function toast ( msg , type = 'ok' ) {
const el = $ ( 'toast' ) ; el . textContent = msg ;
el . className = 'toast show' + ( type === 'error' ? ' error' : '' ) ;
setTimeout ( ( ) => el . className = 'toast' + ( type === 'error' ? ' error' : '' ) , 3000 ) ;
}
function initials ( text ) {
const parts = String ( text || '' ) . replace ( /[@._-]/g , ' ' ) . trim ( ) . split ( /\s+/ ) ;
return ( ( parts [ 0 ] || '' ) . charAt ( 0 ) + ( parts [ 1 ] || '' ) . charAt ( 0 ) ) . toUpperCase ( ) || '?' ;
}
function setUserChip ( ) {
const email = authUser . email || '' ; const name = authUser . name || email ;
if ( $ ( 'user-avatar' ) ) $ ( 'user-avatar' ) . textContent = initials ( name || email ) ;
if ( $ ( 'user-name' ) ) { $ ( 'user-name' ) . textContent = name || email || 'Conectado' ; $ ( 'user-name' ) . title = email ; }
}
async function apiCall ( url ) {
const res = await fetch ( url , { headers : { Accept : 'application/json' } , credentials : 'same-origin' } ) ;
let j ;
try { j = await res . json ( ) ; } catch ( e ) { throw new Error ( 'JSON inválido' ) ; }
if ( res . status === 401 && j . auth _required ) {
window . location . href = j . login _url || '/auth/login.php?return_to=' + encodeURIComponent ( location . pathname ) ;
return null ;
}
if ( ! res . ok || ! j . ok ) throw new Error ( j . error || ( 'HTTP ' + res . status ) ) ;
if ( j . user ) authUser = j . user ;
return j . data ;
}
function titleOf ( t , dept ) {
return t . nombre || t . tipo _tramite || t . tipo _proceso || ( DEPT _LABELS [ dept ] || 'Trámite' ) ;
}
function flatten ( escrituras , tramites ) {
const out = [ ] ;
for ( const e of ( escrituras || [ ] ) ) {
if ( e . fecha _solicitud ) {
out . push ( {
key : 'esc-sol-' + e . id , source : 'esc' , type : 'solicitud' ,
date : ymd ( e . fecha _solicitud ) ,
title : e . tramite || 'Trámite sin nombre' ,
cliente : e . cliente , abogado : e . abogado , estatus : e . estatus ,
raw : e , ref _id : e . id ,
nav : '/escrituras/?edit=' + e . id ,
} ) ;
}
}
for ( const dept of [ 'adm' , 'corp' , 'mig' , 'lit' ] ) {
for ( const t of ( tramites ? . [ dept ] || [ ] ) ) {
const navUrl = '/tramites/?edit=' + encodeURIComponent ( t . codigo ) ;
if ( t . fecha _solicitud ) {
out . push ( {
key : 'tra-sol-' + t . id , source : dept , type : 'solicitud' ,
date : ymd ( t . fecha _solicitud ) ,
title : titleOf ( t , dept ) ,
cliente : t . cliente , abogado : t . abogado , estatus : t . estatus ,
raw : t , ref _id : t . id , codigo : t . codigo , nav : navUrl ,
} ) ;
}
if ( dept === 'mig' && t . fecha _vencimiento ) {
out . push ( {
key : 'tra-venc-' + t . id , source : 'mig' , type : 'vencimiento' ,
date : t . fecha _vencimiento ,
title : titleOf ( t , 'mig' ) + ' — vencimiento' ,
cliente : t . cliente , abogado : t . abogado , estatus : t . estatus ,
raw : t , ref _id : t . id , codigo : t . codigo , nav : navUrl ,
} ) ;
}
if ( dept === 'lit' && t . fecha _termino ) {
out . push ( {
key : 'tra-term-' + t . id , source : 'lit' , type : 'termino' ,
date : t . fecha _termino ,
title : titleOf ( t , 'lit' ) + ' — término procesal' ,
cliente : t . cliente , abogado : t . abogado , estatus : t . estatus ,
raw : t , ref _id : t . id , codigo : t . codigo , nav : navUrl ,
} ) ;
}
}
}
return out
. filter ( e => e . date )
. map ( e => Object . assign ( e , { daysOut : daysToToday ( e . date ) } ) )
. sort ( ( a , b ) => a . date . localeCompare ( b . date ) ) ;
}
async function loadAll ( ) {
setSync ( 'syncing' , 'Cargando…' ) ;
try {
const [ escData , traData ] = await Promise . all ( [
apiCall ( '/escrituras/api.php?action=list' ) ,
apiCall ( '/tramites/api.php?action=list' ) ,
] ) ;
if ( escData === null || traData === null ) return ;
events = flatten ( escData , traData ) ;
setUserChip ( ) ;
setSync ( 'ok' , events . length + ' eventos cargados' ) ;
render ( ) ;
updateKPIs ( ) ;
updateUpcoming ( ) ;
} catch ( e ) {
setSync ( 'error' , 'Sin conexión' ) ;
toast ( 'Error al cargar: ' + e . message , 'error' ) ;
}
}
function visibleEvents ( ) {
const q = search . toLowerCase ( ) . trim ( ) ;
return events . filter ( e => {
if ( ! filters . has ( e . source ) ) return false ;
if ( q ) {
const blob = ( ( e . title || '' ) + ' ' + ( e . cliente || '' ) + ' ' + ( e . abogado || '' ) + ' ' + ( e . codigo || '' ) ) . toLowerCase ( ) ;
if ( ! blob . includes ( q ) ) return false ;
}
return true ;
} ) ;
}
function eventsOnDate ( d ) {
const k = ymd ( d ) ;
return visibleEvents ( ) . filter ( e => e . date === k ) ;
}
function isDeadline ( e ) { return e . type === 'vencimiento' || e . type === 'termino' ; }
function isOpen ( e ) { return ! CLOSED _STATUS . has ( e . estatus || '' ) ; }
function colorFor ( e ) {
if ( isDeadline ( e ) && e . daysOut !== null && isOpen ( e ) ) {
if ( e . daysOut < 0 ) return COLOR _OVERDUE ;
if ( e . daysOut <= 7 ) return COLOR _UPCOMING ;
}
return SRC _COLORS [ e . source ] || '#666' ;
}
function badgeFor ( e ) {
if ( isDeadline ( e ) && e . daysOut !== null && isOpen ( e ) ) {
if ( e . daysOut < 0 ) return { label : 'Vencido' , bg : COLOR _OVERDUE } ;
if ( e . daysOut === 0 ) return { label : 'Hoy' , bg : COLOR _UPCOMING } ;
if ( e . daysOut <= 7 ) return { label : '≤' + e . daysOut + 'd' , bg : COLOR _UPCOMING } ;
}
return null ;
}
function updateKPIs ( ) {
const v = visibleEvents ( ) ;
const deadlines = v . filter ( e => isDeadline ( e ) && isOpen ( e ) ) ;
$ ( 'k-total' ) . textContent = v . length ;
$ ( 'k-7d' ) . textContent = deadlines . filter ( e => e . daysOut !== null && e . daysOut >= 0 && e . daysOut <= 7 ) . length ;
$ ( 'k-30d' ) . textContent = deadlines . filter ( e => e . daysOut !== null && e . daysOut >= 0 && e . daysOut <= 30 ) . length ;
$ ( 'k-overdue' ) . textContent = deadlines . filter ( e => e . daysOut !== null && e . daysOut < 0 ) . length ;
}
function updateUpcoming ( ) {
const wrap = $ ( 'upcoming-row' ) ;
const deadlines = visibleEvents ( )
. filter ( e => isDeadline ( e ) && isOpen ( e ) && e . daysOut !== null )
. sort ( ( a , b ) => a . daysOut - b . daysOut )
. slice ( 0 , 8 ) ;
if ( ! deadlines . length ) {
wrap . innerHTML = '<div class="up-empty">No hay vencimientos próximos en los filtros actuales.</div>' ;
return ;
}
wrap . innerHTML = deadlines . map ( e => {
const over = e . daysOut < 0 ;
const rem = over ? 'Hace ' + Math . abs ( e . daysOut ) + 'd'
: e . daysOut === 0 ? 'HOY'
: 'En ' + e . daysOut + 'd' ;
const d = parseYMD ( e . date ) ;
const ds = d ? d . toLocaleDateString ( 'es-PA' , { day : '2-digit' , month : 'short' } ) : e . date ;
return ` <div class="up-card ${ over ? 'overdue' : '' } " onclick='openEvent(" ${ e . key } ")'>
<div class="up-card-date"> ${ esc ( ds ) } <span class="up-card-rem"> ${ esc ( rem ) } </span></div>
<div class="up-card-title" title=" ${ esc ( e . title ) } "> ${ esc ( e . title ) } </div>
<div class="up-card-meta"> ${ esc ( e . cliente || '—' ) } · ${ esc ( e . abogado || '—' ) } </div>
</div> ` ;
} ) . join ( '' ) ;
}
function setView ( v ) {
view = v ;
document . querySelectorAll ( '#view-tabs button' ) . forEach ( b => b . classList . toggle ( 'active' , b . dataset . view === v ) ) ;
render ( ) ;
}
function setCursor ( d ) { cursor = stripTime ( d ) ; render ( ) ; }
function navPrev ( ) {
if ( view === 'year' ) cursor = new Date ( cursor . getFullYear ( ) - 1 , 0 , 1 ) ;
else if ( view === 'month' ) cursor = addMonths ( cursor , - 1 ) ;
else if ( view === 'week' ) cursor = addDays ( cursor , - 7 ) ;
else if ( view === 'day' ) cursor = addDays ( cursor , - 1 ) ;
setCursor ( cursor ) ;
}
function navNext ( ) {
if ( view === 'year' ) cursor = new Date ( cursor . getFullYear ( ) + 1 , 0 , 1 ) ;
else if ( view === 'month' ) cursor = addMonths ( cursor , 1 ) ;
else if ( view === 'week' ) cursor = addDays ( cursor , 7 ) ;
else if ( view === 'day' ) cursor = addDays ( cursor , 1 ) ;
setCursor ( cursor ) ;
}
function navToday ( ) { setCursor ( new Date ( ) ) ; }
function periodLabel ( ) {
if ( view === 'year' ) return String ( cursor . getFullYear ( ) ) ;
if ( view === 'month' ) return MES _ES [ cursor . getMonth ( ) ] + ' ' + cursor . getFullYear ( ) ;
if ( view === 'week' ) {
const a = startOfWeek ( cursor ) ; const b = addDays ( a , 6 ) ;
return a . getDate ( ) + ' ' + MES _ES [ a . getMonth ( ) ] . slice ( 0 , 3 ) + ' — ' + b . getDate ( ) + ' ' + MES _ES [ b . getMonth ( ) ] . slice ( 0 , 3 ) + ' ' + b . getFullYear ( ) ;
}
return DOW _FULL [ ( cursor . getDay ( ) + 6 ) % 7 ] + ' ' + cursor . getDate ( ) + ' de ' + MES _ES [ cursor . getMonth ( ) ] + ' ' + cursor . getFullYear ( ) ;
}
function render ( ) {
$ ( 'period-label' ) . textContent = periodLabel ( ) ;
if ( view === 'year' ) renderYear ( ) ;
else if ( view === 'month' ) renderMonth ( ) ;
else if ( view === 'week' ) renderWeek ( ) ;
else renderDay ( ) ;
}
function renderYear ( ) {
const year = cursor . getFullYear ( ) ;
const today = stripTime ( new Date ( ) ) ;
const byDate = { } ;
for ( const e of visibleEvents ( ) ) { ( byDate [ e . date ] = byDate [ e . date ] || [ ] ) . push ( e ) ; }
const months = Array . from ( { length : 12 } , ( _ , m ) => {
const start = startOfWeek ( new Date ( year , m , 1 ) ) ;
const cells = [ ] ;
for ( let i = 0 ; i < 42 ; i ++ ) {
const d = addDays ( start , i ) ;
const inMonth = d . getMonth ( ) === m && d . getFullYear ( ) === year ;
const key = ymd ( d ) ;
const evs = byDate [ key ] || [ ] ;
const hasDeadline = evs . some ( e => isDeadline ( e ) && isOpen ( e ) ) ;
const dots = evs . slice ( 0 , 4 ) . map ( e =>
` <span class="dot" style="background: ${ colorFor ( e ) } "></span> `
) . join ( '' ) ;
cells . push ( ` <div class="year-mini-day ${ inMonth ? '' : 'dim' } ${ sameDate ( d , today ) ? 'today' : '' } ${ hasDeadline ? 'has-deadline' : '' } "> ${ d . getDate ( ) } <span class="dots"> ${ dots } </span></div> ` ) ;
}
return ` <div class="year-mini" onclick="jumpToMonth( ${ m } )">
<div class="year-mini-title"> ${ MES _ES [ m ] } </div>
<div class="year-mini-grid">
${ DOW _SHORT . map ( d => ` <div class="year-mini-dow"> ${ d } </div> ` ) . join ( '' ) }
${ cells . join ( '' ) }
</div>
</div> ` ;
} ) . join ( '' ) ;
$ ( 'cal-main' ) . innerHTML = ` <div class="year-grid"> ${ months } </div> ` ;
}
function jumpToMonth ( m ) {
cursor = new Date ( cursor . getFullYear ( ) , m , 1 ) ;
setView ( 'month' ) ;
}
function renderMonth ( ) {
const today = stripTime ( new Date ( ) ) ;
const start = startOfWeek ( startOfMonth ( cursor ) ) ;
const cells = [ ] ;
for ( let i = 0 ; i < 42 ; i ++ ) {
const d = addDays ( start , i ) ;
const inMonth = d . getMonth ( ) === cursor . getMonth ( ) ;
const isToday = sameDate ( d , today ) ;
const evs = eventsOnDate ( d ) ;
const max = 3 ;
const chips = evs . slice ( 0 , max ) . map ( e => {
const b = badgeFor ( e ) ;
const cls = ( b && b . bg === COLOR _OVERDUE ) ? 'evt-chip overdue' : 'evt-chip' ;
const label = b ? ( e . title + ' · ' + b . label ) : e . title ;
return ` <div class=" ${ cls } " style="background: ${ colorFor ( e ) } " onclick='event.stopPropagation();openEvent(" ${ e . key } ")' title=" ${ esc ( label ) } — ${ esc ( e . cliente || '' ) } "> ${ esc ( e . title ) } </div> ` ;
} ) . join ( '' ) ;
const more = evs . length > max ? ` <div class="evt-more">+ ${ evs . length - max } más</div> ` : '' ;
cells . push ( ` <div class="month-cell ${ inMonth ? '' : 'dim' } ${ isToday ? 'today' : '' } " onclick="goToDay(' ${ ymd ( d ) } ')">
<span class="cell-num"> ${ d . getDate ( ) } </span>
${ chips } ${ more }
</div> ` ) ;
}
$ ( 'cal-main' ) . innerHTML = ` <div class="month-grid">
${ DOW _SHORT . map ( d => ` <div class="month-dow"> ${ d } </div> ` ) . join ( '' ) }
${ cells . join ( '' ) }
</div> ` ;
}
function goToDay ( ymdStr ) {
cursor = parseYMD ( ymdStr ) ;
setView ( 'day' ) ;
}
function renderWeek ( ) {
const today = stripTime ( new Date ( ) ) ;
const start = startOfWeek ( cursor ) ;
const cols = [ ] ;
for ( let i = 0 ; i < 7 ; i ++ ) {
const d = addDays ( start , i ) ;
const isToday = sameDate ( d , today ) ;
const evs = eventsOnDate ( d ) ;
const items = evs . length ? evs . map ( e => {
const b = badgeFor ( e ) ;
return ` <div class="week-evt" style="background: ${ colorFor ( e ) } " onclick='event.stopPropagation();openEvent(" ${ e . key } ")'>
<div class="tt"> ${ esc ( e . title ) } </div>
<div class="meta"> ${ esc ( e . cliente || '—' ) } ${ b ? ' · ' + esc ( b . label ) : '' } </div>
</div> ` ;
} ) . join ( '' ) : '<div class="week-empty">Sin eventos</div>' ;
cols . push ( ` <div class="week-col">
<div class="week-col-head ${ isToday ? 'today' : '' } ">
<div class="week-col-day"> ${ DOW _SHORT [ i ] } </div>
<div class="week-col-num"> ${ d . getDate ( ) } </div>
</div>
<div class="week-col-body" onclick="goToDay(' ${ ymd ( d ) } ')"> ${ items } </div>
</div> ` ) ;
}
$ ( 'cal-main' ) . innerHTML = ` <div class="week-grid"> ${ cols . join ( '' ) } </div> ` ;
}
function renderDay ( ) {
const today = stripTime ( new Date ( ) ) ;
const evs = eventsOnDate ( cursor ) ;
const isToday = sameDate ( cursor , today ) ;
const heading = DOW _FULL [ ( cursor . getDay ( ) + 6 ) % 7 ] + ' ' + cursor . getDate ( ) + ' de ' + MES _ES [ cursor . getMonth ( ) ] ;
let html = ` <div class="day-view">
<div class="day-header"> ${ heading } ${ isToday ? ' · Hoy' : '' } </div>
<div class="day-sub"> ${ evs . length } ${ evs . length === 1 ? 'evento' : 'eventos' } </div> ` ;
if ( ! evs . length ) {
html += ` <div class="cal-empty">No hay eventos este día.</div> ` ;
} else {
html += evs . map ( e => {
const b = badgeFor ( e ) ;
const tag = b ? ` <span class="evt-tag" style="background: ${ b . bg } ;color:#fff"> ${ esc ( b . label ) } </span> ` : '' ;
const srcTag = ` <span class="evt-tag" style="background: ${ SRC _COLORS [ e . source ] || '#888' } ;color:#fff"> ${ esc ( SRC _LABELS [ e . source ] || '' ) } </span> ` ;
return ` <div class="day-evt" onclick='openEvent(" ${ e . key } ")'>
<div class="day-evt-bar" style="background: ${ colorFor ( e ) } "></div>
<div>
<div class="day-evt-title"> ${ esc ( e . title ) } </div>
<div class="day-evt-meta"> ${ esc ( e . cliente || '—' ) } · ${ esc ( e . abogado || '—' ) } · ${ esc ( e . estatus || '' ) } </div>
</div>
<div class="day-evt-badges"> ${ tag } ${ srcTag } </div>
</div> ` ;
} ) . join ( '' ) ;
}
html += '</div>' ;
$ ( 'cal-main' ) . innerHTML = html ;
}
function openEvent ( key ) {
const e = events . find ( x => x . key === key ) ;
if ( ! e ) { toast ( 'Evento no encontrado' , 'error' ) ; return ; }
selected = e ;
$ ( 'evt-title' ) . textContent = e . title ;
$ ( 'evt-body' ) . innerHTML = renderEventBody ( e ) ;
$ ( 'btn-edit' ) . onclick = ( ) => { window . location . href = e . nav ; } ;
$ ( 'evt-modal' ) . classList . add ( 'open' ) ;
}
function closeEvent ( ) {
selected = null ;
$ ( 'evt-modal' ) . classList . remove ( 'open' ) ;
}
function renderEventBody ( e ) {
const srcColor = SRC _COLORS [ e . source ] ;
const srcLabel = SRC _LABELS [ e . source ] ;
const tEvt = ( { solicitud : 'Fecha de Solicitud' , vencimiento : 'Fecha de Vencimiento' , termino : 'Fecha de Término' } ) [ e . type ] || e . type ;
const d = parseYMD ( e . date ) ;
const dStr = d ? d . toLocaleDateString ( 'es-PA' , { weekday : 'long' , day : '2-digit' , month : 'long' , year : 'numeric' } ) : e . date ;
const r = e . raw || { } ;
const rows = [ ] ;
rows . push ( [ 'Origen' , ` <span class="src-tag" style="background: ${ srcColor } "> ${ esc ( srcLabel ) } </span> ` ] ) ;
if ( e . codigo ) rows . push ( [ 'Código' , ` <b> ${ esc ( e . codigo ) } </b> ` ] ) ;
rows . push ( [ tEvt , ` <b style="text-transform:capitalize"> ${ esc ( dStr ) } </b> ` ] ) ;
if ( e . daysOut !== null && isDeadline ( e ) ) {
let lbl ;
if ( ! isOpen ( e ) ) lbl = ` <span style="color:var(--gray-400)">Cerrado ( ${ esc ( e . estatus ) } )</span> ` ;
else if ( e . daysOut < 0 ) lbl = ` <span style="color: ${ COLOR _OVERDUE } "><b>Vencido hace ${ Math . abs ( e . daysOut ) } día(s)</b></span> ` ;
else if ( e . daysOut === 0 ) lbl = ` <span style="color: ${ COLOR _UPCOMING } "><b>Vence HOY</b></span> ` ;
else lbl = ` Faltan <b> ${ e . daysOut } </b> día(s) ` ;
rows . push ( [ 'Restante' , lbl ] ) ;
}
rows . push ( [ 'Cliente' , esc ( e . cliente || '—' ) ] ) ;
rows . push ( [ 'Abogado' , esc ( e . abogado || '—' ) ] ) ;
rows . push ( [ 'Estatus' , esc ( e . estatus || '—' ) ] ) ;
if ( e . source === 'esc' ) {
if ( r . notaria ) rows . push ( [ 'Notaría' , esc ( r . notaria ) ] ) ;
if ( r . inscribe ) rows . push ( [ 'Se inscribe' , esc ( r . inscribe ) ] ) ;
if ( r . altera _turno ) rows . push ( [ 'Altera turno' , esc ( r . altera _turno ) ] ) ;
if ( r . entrada ) rows . push ( [ 'Entrada' , '<span style="font-variant-numeric:tabular-nums">' + esc ( r . entrada ) + '</span>' ] ) ;
}
if ( e . source === 'adm' && r . institucion ) rows . push ( [ 'Institución' , esc ( r . institucion ) ] ) ;
if ( e . source === 'corp' && r . tipo _documento ) rows . push ( [ 'Tipo doc.' , esc ( r . tipo _documento ) ] ) ;
if ( e . source === 'mig' ) {
if ( r . tipo _tramite ) rows . push ( [ 'Tipo trámite' , esc ( r . tipo _tramite ) ] ) ;
if ( r . doc _completa ) rows . push ( [ 'Doc. completa' , esc ( r . doc _completa ) ] ) ;
if ( r . permiso _trabajo ) rows . push ( [ 'Permiso trabajo' , esc ( r . permiso _trabajo ) ] ) ;
if ( r . dependientes ) rows . push ( [ 'Dependientes' , esc ( r . dependientes ) ] ) ;
}
if ( e . source === 'lit' ) {
if ( r . tipo _proceso ) rows . push ( [ 'Tipo proceso' , esc ( r . tipo _proceso ) ] ) ;
if ( r . juzgado ) rows . push ( [ 'Juzgado' , esc ( r . juzgado ) ] ) ;
if ( r . esfera ) rows . push ( [ 'Esfera' , esc ( r . esfera ) ] ) ;
if ( r . instancia ) rows . push ( [ 'Instancia' , esc ( r . instancia ) ] ) ;
if ( r . circuito ) rows . push ( [ 'Circuito' , esc ( r . circuito ) ] ) ;
}
if ( r . instrucciones ) rows . push ( [ 'Instrucciones' , '<span style="white-space:pre-wrap">' + esc ( r . instrucciones ) + '</span>' ] ) ;
if ( r . observaciones ) rows . push ( [ 'Observaciones' , '<span style="white-space:pre-wrap">' + esc ( r . observaciones ) + '</span>' ] ) ;
return rows . map ( ( [ k , v ] ) => ` <div class="row"><div class="k"> ${ k } </div><div class="v"> ${ v } </div></div> ` ) . join ( '' ) ;
}
document . querySelectorAll ( '#view-tabs button' ) . forEach ( b => {
b . addEventListener ( 'click' , ( ) => setView ( b . dataset . view ) ) ;
} ) ;
$ ( 'nav-prev' ) . addEventListener ( 'click' , navPrev ) ;
$ ( 'nav-next' ) . addEventListener ( 'click' , navNext ) ;
$ ( 'nav-today' ) . addEventListener ( 'click' , navToday ) ;
$ ( 'search' ) . addEventListener ( 'input' , e => {
search = e . target . value ;
render ( ) ; updateKPIs ( ) ; updateUpcoming ( ) ;
} ) ;
document . querySelectorAll ( '.chip[data-src]' ) . forEach ( ch => {
ch . addEventListener ( 'click' , ( ) => {
const s = ch . dataset . src ;
if ( filters . has ( s ) ) filters . delete ( s ) ; else filters . add ( s ) ;
ch . classList . toggle ( 'on' , filters . has ( s ) ) ;
ch . classList . toggle ( 'off' , ! filters . has ( s ) ) ;
render ( ) ; updateKPIs ( ) ; updateUpcoming ( ) ;
} ) ;
} ) ;
document . addEventListener ( 'keydown' , e => {
if ( /^(INPUT|TEXTAREA|SELECT)$/ . test ( e . target . tagName ) ) {
if ( e . key === 'Escape' && e . target . id === 'search' ) {
e . target . value = '' ; search = '' ; render ( ) ; updateKPIs ( ) ; updateUpcoming ( ) ;
}
return ;
}
if ( e . key === 'Escape' ) { closeEvent ( ) ; return ; }
if ( e . key === 'ArrowLeft' ) navPrev ( ) ;
if ( e . key === 'ArrowRight' ) navNext ( ) ;
if ( e . key === 't' || e . key === 'T' ) navToday ( ) ;
if ( e . key === 'y' || e . key === 'Y' ) setView ( 'year' ) ;
if ( e . key === 'm' || e . key === 'M' ) setView ( 'month' ) ;
if ( e . key === 'w' || e . key === 'W' ) setView ( 'week' ) ;
if ( e . key === 'd' || e . key === 'D' ) setView ( 'day' ) ;
} ) ;
setInterval ( ( ) => { if ( ! $ ( 'evt-modal' ) . classList . contains ( 'open' ) ) loadAll ( ) ; } , 60000 ) ;
loadAll ( ) ;
< / script >
< / body >
< / html >