From 6dc2144db79ed63d62adfcb8f11a62490fefd731 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 6 May 2026 23:42:37 -0500 Subject: [PATCH] Phase 6: portal, website intake, calculator, and case law library - controllers/portal.py: FamilyLawPortal with 8 routes (cases list, case detail, calculator pre-fill, caselaw library, deadline complete AJAX, public intake landing/form/submit) - views/portal_case_templates.xml: portal home card, case list, full case detail with timeline widget, AI summary, DV safety banner - views/portal_calculator_templates.xml: FL 61.30 interactive calculator - views/portal_caselaw_templates.xml: searchable case law library (EN/ES) - views/website_intake_templates.xml: public 4-step intake form with DV quick-exit, fee waiver, and intake confirmation page - static/src/css/familylaw_portal.css: full portal/website CSS (EN/ES lang toggle, deadline card color coding, timeline, AI summary box) - static/src/js/fl_calculator.js: FL 61.30 schedule lookup, above- schedule formula, FL 61.30(11)(b) substantial timesharing calculation - static/src/js/fl_timeline.js: deadline timeline widget with filter buttons and mark-complete AJAX - __init__.py: import controllers package - __manifest__.py: add Phase 6 portal view files Co-Authored-By: Claude Sonnet 4.6 --- activeblue_familylaw/__init__.py | 1 + activeblue_familylaw/__manifest__.py | 5 + activeblue_familylaw/controllers/__init__.py | 1 + activeblue_familylaw/controllers/portal.py | 317 +++++++++++ .../static/src/css/familylaw_portal.css | 419 ++++++++++++++- .../static/src/js/fl_calculator.js | 362 ++++++++++++- .../static/src/js/fl_timeline.js | 153 +++++- .../views/portal_calculator_templates.xml | 211 ++++++++ .../views/portal_case_templates.xml | 316 +++++++++++ .../views/portal_caselaw_templates.xml | 201 +++++++ .../views/website_intake_templates.xml | 502 ++++++++++++++++++ 11 files changed, 2456 insertions(+), 32 deletions(-) create mode 100644 activeblue_familylaw/controllers/__init__.py create mode 100644 activeblue_familylaw/controllers/portal.py create mode 100644 activeblue_familylaw/views/portal_calculator_templates.xml create mode 100644 activeblue_familylaw/views/portal_case_templates.xml create mode 100644 activeblue_familylaw/views/portal_caselaw_templates.xml create mode 100644 activeblue_familylaw/views/website_intake_templates.xml diff --git a/activeblue_familylaw/__init__.py b/activeblue_familylaw/__init__.py index 9b42961..6bac603 100644 --- a/activeblue_familylaw/__init__.py +++ b/activeblue_familylaw/__init__.py @@ -1,2 +1,3 @@ from . import models from . import wizard +from . import controllers diff --git a/activeblue_familylaw/__manifest__.py b/activeblue_familylaw/__manifest__.py index 56297de..f3acc14 100644 --- a/activeblue_familylaw/__manifest__.py +++ b/activeblue_familylaw/__manifest__.py @@ -65,6 +65,11 @@ 'report/report_mandatory_disclosure.xml', 'report/report_default_motion.xml', 'report/report_parenting_plan.xml', + # Phase 6 — Portal & Website Templates + 'views/portal_case_templates.xml', + 'views/portal_calculator_templates.xml', + 'views/portal_caselaw_templates.xml', + 'views/website_intake_templates.xml', ], 'assets': { 'web.assets_frontend': [ diff --git a/activeblue_familylaw/controllers/__init__.py b/activeblue_familylaw/controllers/__init__.py new file mode 100644 index 0000000..8c3feb6 --- /dev/null +++ b/activeblue_familylaw/controllers/__init__.py @@ -0,0 +1 @@ +from . import portal diff --git a/activeblue_familylaw/controllers/portal.py b/activeblue_familylaw/controllers/portal.py new file mode 100644 index 0000000..197cd64 --- /dev/null +++ b/activeblue_familylaw/controllers/portal.py @@ -0,0 +1,317 @@ +import json +import logging +from datetime import date, timedelta + +from odoo import http +from odoo.http import request +from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager + +_logger = logging.getLogger(__name__) + + +class FamilyLawPortal(CustomerPortal): + """ + Phase 6 — Portal controller for fl.case, fl.caselaw, calculator, and intake. + + Routes: + GET /my/cases — list of petitioner/respondent cases + GET /my/cases/ — case detail with deadlines/timeline + GET /my/cases/calculator — FL 61.30 interactive calculator + GET /my/cases/caselaw — FL case law library (searchable) + POST /family-law/deadline/complete — AJAX: mark deadline complete + GET /family-law/intake — public landing page + GET /family-law/intake/start — multi-step intake form + POST /family-law/intake/submit — process intake form → create case + """ + + # ────────────────────────────────────────────────────────────────────────── + # Portal home — add case_count for the "My Cases" card + # ────────────────────────────────────────────────────────────────────────── + + def _prepare_home_portal_values(self, counters): + values = super()._prepare_home_portal_values(counters) + if 'case_count' in counters: + partner = request.env.user.partner_id + cases = request.env['fl.case'].search([ + '|', + ('petitioner_id.user_ids', 'in', request.env.user.ids), + ('respondent_id.user_ids', 'in', request.env.user.ids), + ]) + values['case_count'] = len(cases) + return values + + # ────────────────────────────────────────────────────────────────────────── + # /my/cases — case list + # ────────────────────────────────────────────────────────────────────────── + + @http.route('/my/cases', type='http', auth='user', website=True) + def portal_my_cases(self, **kwargs): + cases = request.env['fl.case'].search([ + '|', + ('petitioner_id.user_ids', 'in', request.env.user.ids), + ('respondent_id.user_ids', 'in', request.env.user.ids), + ], order='create_date desc') + + return request.render('activeblue_familylaw.portal_my_cases', { + 'cases': cases, + 'page_name': 'cases', + }) + + # ────────────────────────────────────────────────────────────────────────── + # /my/cases/ — case detail + # ────────────────────────────────────────────────────────────────────────── + + @http.route('/my/cases/', type='http', auth='user', website=True) + def portal_case_detail(self, case_id, **kwargs): + # Access check: user must be petitioner or respondent + case = request.env['fl.case'].search([ + ('id', '=', case_id), + '|', + ('petitioner_id.user_ids', 'in', request.env.user.ids), + ('respondent_id.user_ids', 'in', request.env.user.ids), + ], limit=1) + + if not case: + return request.render('website.403') + + latest_analysis = case.analysis_ids[:1] if case.analysis_ids else None + + return request.render('activeblue_familylaw.portal_case_detail', { + 'case': case, + 'latest_analysis': latest_analysis, + 'page_name': 'case_detail', + }) + + # ────────────────────────────────────────────────────────────────────────── + # /my/cases/calculator — FL 61.30 calculator + # ────────────────────────────────────────────────────────────────────────── + + @http.route('/my/cases/calculator', type='http', auth='public', website=True) + def portal_calculator(self, case_id=None, **kwargs): + """ + Pre-fill calculator from case data if case_id is provided and user is authenticated. + """ + prefill_petitioner = None + prefill_respondent = None + + if case_id and request.env.user and request.env.user.id != request.env.ref('base.public_user').id: + case = request.env['fl.case'].search([ + ('id', '=', int(case_id)), + '|', + ('petitioner_id.user_ids', 'in', request.env.user.ids), + ('respondent_id.user_ids', 'in', request.env.user.ids), + ], limit=1) + if case: + prefill_petitioner = case.petitioner_net_income + prefill_respondent = case.respondent_net_income + + return request.render('activeblue_familylaw.portal_calculator', { + 'prefill_petitioner': prefill_petitioner, + 'prefill_respondent': prefill_respondent, + 'page_name': 'calculator', + }) + + # ────────────────────────────────────────────────────────────────────────── + # /my/cases/caselaw — case law library + # ────────────────────────────────────────────────────────────────────────── + + @http.route('/my/cases/caselaw', type='http', auth='public', website=True) + def portal_caselaw(self, tag=None, court=None, **kwargs): + domain = [('active', '=', True)] + if tag: + domain += [('issue_tag_ids.name', '=', tag)] + if court: + domain += [('court', '=', court)] + + cases = request.env['fl.caselaw'].search(domain, order='year desc, short_name') + + return request.render('activeblue_familylaw.portal_caselaw', { + 'cases': cases, + 'page_name': 'caselaw', + }) + + # ────────────────────────────────────────────────────────────────────────── + # AJAX: /family-law/deadline/complete + # ────────────────────────────────────────────────────────────────────────── + + @http.route('/family-law/deadline/complete', type='json', auth='user', methods=['POST'], csrf=True) + def deadline_complete(self, deadline_id=None, **kwargs): + if not deadline_id: + return {'success': False, 'error': 'Missing deadline_id'} + + deadline = request.env['fl.deadline'].search([ + ('id', '=', deadline_id), + '|', + ('case_id.petitioner_id.user_ids', 'in', request.env.user.ids), + ('case_id.respondent_id.user_ids', 'in', request.env.user.ids), + ], limit=1) + + if not deadline: + return {'success': False, 'error': 'Deadline not found or access denied'} + + try: + deadline.action_complete() + return {'success': True} + except Exception as e: + _logger.error("Portal deadline complete error: %s", e) + return {'success': False, 'error': str(e)} + + # ────────────────────────────────────────────────────────────────────────── + # Public: /family-law/intake — landing page + # ────────────────────────────────────────────────────────────────────────── + + @http.route('/family-law/intake', type='http', auth='public', website=True) + def intake_landing(self, **kwargs): + return request.render('activeblue_familylaw.website_intake_landing', { + 'page_name': 'intake', + }) + + # ────────────────────────────────────────────────────────────────────────── + # Public: /family-law/intake/start — multi-step form + # ────────────────────────────────────────────────────────────────────────── + + @http.route('/family-law/intake/start', type='http', auth='public', website=True) + def intake_start(self, case_type=None, **kwargs): + return request.render('activeblue_familylaw.website_intake_form', { + 'case_type': case_type or 'child_support', + 'page_name': 'intake_form', + }) + + # ────────────────────────────────────────────────────────────────────────── + # Public: POST /family-law/intake/submit — process intake → create case + # ────────────────────────────────────────────────────────────────────────── + + @http.route('/family-law/intake/submit', type='http', auth='public', website=True, methods=['POST'], csrf=True) + def intake_submit(self, **post): + """ + Process intake form submission. + Creates or finds partner records, creates fl.case, triggers fee waiver check + and optional AI analysis, then redirects to confirmation page. + """ + try: + petitioner_name = (post.get('petitioner_name') or '').strip() + petitioner_email = (post.get('petitioner_email') or '').strip() + respondent_name = (post.get('respondent_name') or '').strip() + case_type = post.get('case_type') or 'child_support' + num_children = int(post.get('num_children') or 1) + court_case_number = (post.get('court_case_number') or '').strip() or False + domestic_violence = post.get('domestic_violence') == 'yes' + respondent_has_counsel = post.get('respondent_has_counsel') == 'yes' + income_concern = post.get('income_imputation_concern') == 'yes' + petitioner_income = float(post.get('petitioner_income') or 0) + respondent_income = float(post.get('respondent_income') or 0) + current_order = float(post.get('current_order_amount') or 0) + household_size = int(post.get('household_size') or 3) + fee_waiver_request = post.get('fee_waiver_request') == 'yes' + notes = (post.get('notes') or '').strip() + fl_resident_since = post.get('fl_resident_since') or False + + # Use sudo for partner/case creation (public user has limited rights) + env = request.env['fl.case'].sudo() + + # Find or create petitioner partner + petitioner = request.env['res.partner'].sudo().search( + [('email', '=', petitioner_email)], limit=1 + ) + if not petitioner: + petitioner = request.env['res.partner'].sudo().create({ + 'name': petitioner_name, + 'email': petitioner_email, + 'phone': post.get('petitioner_phone') or False, + 'street': post.get('petitioner_address') or False, + 'city': post.get('petitioner_city') or False, + 'state_id': request.env.ref('base.state_us_10').id, # FL + 'country_id': request.env.ref('base.us').id, + }) + + # Find or create respondent partner + respondent = False + if respondent_name: + respondent = request.env['res.partner'].sudo().search( + [('name', '=', respondent_name)], limit=1 + ) + if not respondent: + respondent = request.env['res.partner'].sudo().create({ + 'name': respondent_name, + }) + + # Build fl.party records (minimal) + petitioner_party = request.env['fl.party'].sudo().search( + [('partner_id', '=', petitioner.id)], limit=1 + ) + if not petitioner_party: + petitioner_party = request.env['fl.party'].sudo().create({ + 'name': petitioner_name, + 'email': petitioner_email, + 'employment_status': 'employed', + 'monthly_gross_income': petitioner_income, + }) + + respondent_party = False + if respondent: + respondent_party = request.env['fl.party'].sudo().search( + [('partner_id', '=', respondent.id)], limit=1 + ) + if not respondent_party: + respondent_party = request.env['fl.party'].sudo().create({ + 'name': respondent_name, + 'monthly_gross_income': respondent_income, + }) + + # Parse FL resident since date + filing_date = date.today() + residency_ok = True + if fl_resident_since: + try: + res_date = date.fromisoformat(fl_resident_since) + residency_ok = (filing_date - res_date).days >= 180 + except ValueError: + pass + + # Create the case + case_vals = { + 'case_type': case_type, + 'petitioner_id': petitioner_party.id, + 'respondent_id': respondent_party.id if respondent_party else False, + 'court_case_number': court_case_number, + 'domestic_violence_flag': domestic_violence, + 'respondent_has_counsel': respondent_has_counsel, + 'current_order_amount': current_order, + 'filing_date': filing_date, + 'residency_requirement_met': residency_ok, + 'household_size': household_size, + } + if notes: + case_vals['description'] = notes + + case = request.env['fl.case'].sudo().create(case_vals) + + # Fee waiver check + attorney_referral = domestic_violence or respondent_has_counsel + fee_waiver_eligible = False + if fee_waiver_request: + fpl = request.env['fl.fee.waiver'].sudo().search([('case_id', '=', case.id)], limit=1) + if fpl: + fee_waiver_eligible = fpl.is_eligible + + # Trigger AI analysis (async-ish: do it now, portal will show result) + try: + request.env['fl.ai.engine'].sudo().analyze_case(case.id) + except Exception as e: + _logger.warning("Portal intake: AI analysis failed for case %s: %s", case.id, e) + + return request.render('activeblue_familylaw.website_intake_confirm', { + 'case_name': case.name, + 'petitioner_email': petitioner_email, + 'fee_waiver_eligible': fee_waiver_eligible, + 'attorney_referral': attorney_referral, + 'page_name': 'intake_confirm', + }) + + except Exception as exc: + _logger.error("Intake submission error: %s", exc, exc_info=True) + return request.render('activeblue_familylaw.website_intake_form', { + 'case_type': post.get('case_type', 'child_support'), + 'error': str(exc), + 'page_name': 'intake_form', + }) diff --git a/activeblue_familylaw/static/src/css/familylaw_portal.css b/activeblue_familylaw/static/src/css/familylaw_portal.css index 1106e71..7b43f4e 100644 --- a/activeblue_familylaw/static/src/css/familylaw_portal.css +++ b/activeblue_familylaw/static/src/css/familylaw_portal.css @@ -1,29 +1,93 @@ -/* ActiveBlue Family Law — Portal CSS - Phase 6: Full portal styling - Phase 1: Stub -*/ +/* ============================================================ + ActiveBlue Family Law — Portal CSS + Phase 6: Full portal styling for fl.case portal pages. + Miami-Dade 11th Circuit — Pro Se litigant UX + ============================================================ */ -/* Attorney referral banner */ +/* ─── Base typography ─────────────────────────────────────── */ +.fl-portal { + font-family: 'Segoe UI', Arial, sans-serif; + color: #2d3748; + line-height: 1.6; +} + +/* ─── Page header (case name banner) ─────────────────────── */ +.fl-case-header { + background: linear-gradient(135deg, #003366 0%, #005ca8 100%); + color: #fff; + border-radius: 8px; + padding: 24px 28px; + margin-bottom: 24px; +} +.fl-case-header h2 { + margin: 0 0 4px 0; + font-size: 1.5rem; +} +.fl-case-header .fl-case-meta { + font-size: 0.85rem; + opacity: 0.85; +} +.fl-case-header .badge { + font-size: 0.8rem; + padding: 4px 10px; + border-radius: 12px; + background: rgba(255,255,255,0.2); + margin-left: 8px; + vertical-align: middle; +} + +/* ─── Attorney referral banner ────────────────────────────── */ .fl-attorney-referral-banner { background: #f8d7da; border: 2px solid #dc3545; - border-radius: 6px; - padding: 16px; + border-radius: 8px; + padding: 16px 20px; margin-bottom: 20px; color: #721c24; - font-weight: bold; +} +.fl-attorney-referral-banner .fl-banner-icon { + font-size: 1.4rem; + margin-right: 8px; +} +.fl-attorney-referral-banner h5 { + color: #721c24; + margin-bottom: 6px; +} +.fl-attorney-referral-banner a { + color: #721c24; + text-decoration: underline; } -/* DV safety banner */ +/* ─── DV safety banner ────────────────────────────────────── */ .fl-dv-safety-banner { background: #fff3cd; - border: 2px solid #ffc107; - border-radius: 6px; - padding: 16px; + border: 2px solid #856404; + border-radius: 8px; + padding: 16px 20px; margin-bottom: 20px; + color: #533f03; +} +.fl-dv-safety-banner h5 { + color: #533f03; + margin-bottom: 6px; +} +.fl-dv-safety-escape-link { + display: inline-block; + background: #dc3545; + color: #fff; + padding: 6px 14px; + border-radius: 4px; + text-decoration: none; + font-weight: bold; + margin-top: 8px; +} +.fl-dv-safety-escape-link:hover { + background: #b02a37; + color: #fff; + text-decoration: none; } -/* Deadline urgency colors */ +/* ─── Deadline status colors ──────────────────────────────── */ .fl-deadline-overdue { color: #dc3545; font-weight: bold; @@ -32,25 +96,336 @@ color: #fd7e14; font-weight: bold; } +.fl-deadline-soon { + color: #856404; +} .fl-deadline-ok { - color: #28a745; + color: #198754; +} +.fl-deadline-completed { + color: #6c757d; + text-decoration: line-through; } -/* Support calculator */ +/* ─── Deadline card ───────────────────────────────────────── */ +.fl-deadline-card { + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 14px 16px; + margin-bottom: 10px; + background: #fff; + transition: box-shadow 0.2s; +} +.fl-deadline-card:hover { + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} +.fl-deadline-card.overdue { + border-left: 4px solid #dc3545; + background: #fff5f5; +} +.fl-deadline-card.urgent { + border-left: 4px solid #fd7e14; + background: #fff8f0; +} +.fl-deadline-card.completed { + border-left: 4px solid #198754; + background: #f8fdf9; + opacity: 0.7; +} +.fl-deadline-card .fl-deadline-type { + font-size: 0.78rem; + color: #6c757d; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.fl-deadline-card .fl-deadline-name { + font-weight: 600; + margin: 2px 0; +} +.fl-deadline-card .fl-deadline-date { + font-size: 0.9rem; +} + +/* ─── Support calculator ──────────────────────────────────── */ +.fl-calculator-section { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 10px; + padding: 28px; + margin-bottom: 24px; +} +.fl-calculator-section h4 { + color: #003366; + margin-bottom: 16px; + font-size: 1.1rem; +} .fl-calculator-result { - background: #d4edda; - border: 1px solid #28a745; - border-radius: 6px; - padding: 20px; + background: linear-gradient(135deg, #d4edda, #c3e6cb); + border: 2px solid #198754; + border-radius: 8px; + padding: 24px; text-align: center; - font-size: 1.4em; + font-size: 1.6rem; font-weight: bold; + color: #0a3622; + margin: 24px 0; +} +.fl-calculator-result .fl-result-label { + font-size: 0.85rem; + font-weight: normal; color: #155724; - margin: 20px 0; + margin-bottom: 4px; +} +.fl-calculator-breakdown { + background: #fff; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 16px; + margin-top: 12px; + font-size: 0.9rem; +} +.fl-calculator-breakdown table { + width: 100%; + border-collapse: collapse; +} +.fl-calculator-breakdown td { + padding: 4px 8px; +} +.fl-calculator-breakdown td:last-child { + text-align: right; + font-weight: 600; +} +.fl-calculator-breakdown tr.total-row { + border-top: 2px solid #003366; + font-size: 1.05rem; + color: #003366; } -/* Bilingual toggle */ +/* ─── Section cards ───────────────────────────────────────── */ +.fl-section-card { + background: #fff; + border: 1px solid #dee2e6; + border-radius: 8px; + margin-bottom: 20px; + overflow: hidden; +} +.fl-section-card .fl-section-header { + background: #003366; + color: #fff; + padding: 10px 16px; + font-weight: 600; + font-size: 0.95rem; +} +.fl-section-card .fl-section-body { + padding: 16px; +} + +/* ─── Timeline ────────────────────────────────────────────── */ +.fl-timeline { + position: relative; + padding-left: 28px; +} +.fl-timeline::before { + content: ''; + position: absolute; + left: 10px; + top: 0; + bottom: 0; + width: 2px; + background: #dee2e6; +} +.fl-timeline-item { + position: relative; + margin-bottom: 20px; +} +.fl-timeline-item::before { + content: ''; + position: absolute; + left: -22px; + top: 6px; + width: 12px; + height: 12px; + border-radius: 50%; + background: #003366; + border: 2px solid #fff; + box-shadow: 0 0 0 2px #003366; +} +.fl-timeline-item.overdue::before { + background: #dc3545; + box-shadow: 0 0 0 2px #dc3545; +} +.fl-timeline-item.completed::before { + background: #198754; + box-shadow: 0 0 0 2px #198754; +} +.fl-timeline-item .fl-timeline-date { + font-size: 0.78rem; + color: #6c757d; + margin-bottom: 2px; +} +.fl-timeline-item .fl-timeline-title { + font-weight: 600; +} +.fl-timeline-item .fl-timeline-desc { + font-size: 0.88rem; + color: #495057; +} + +/* ─── Document list ───────────────────────────────────────── */ +.fl-document-item { + display: flex; + align-items: center; + padding: 10px 14px; + border-bottom: 1px solid #f0f0f0; + text-decoration: none; + color: #2d3748; + transition: background 0.15s; +} +.fl-document-item:hover { + background: #f8f9fa; + text-decoration: none; + color: #2d3748; +} +.fl-document-item .fl-doc-icon { + width: 36px; + height: 36px; + border-radius: 6px; + background: #e9ecef; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + flex-shrink: 0; + font-size: 1.1rem; + color: #003366; +} +.fl-document-item .fl-doc-name { + font-weight: 600; + font-size: 0.95rem; +} +.fl-document-item .fl-doc-type { + font-size: 0.78rem; + color: #6c757d; +} +.fl-document-item .fl-doc-badge { + margin-left: auto; + font-size: 0.75rem; + padding: 3px 8px; + border-radius: 10px; + background: #e9ecef; + color: #495057; +} +.fl-document-item .fl-doc-badge.filed { + background: #d4edda; + color: #0a3622; +} +.fl-document-item .fl-doc-badge.draft { + background: #fff3cd; + color: #533f03; +} + +/* ─── AI summary box ──────────────────────────────────────── */ +.fl-ai-summary { + background: linear-gradient(135deg, #e8f4fd, #dbeafe); + border: 1px solid #93c5fd; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; +} +.fl-ai-summary .fl-ai-header { + font-weight: 700; + color: #1e40af; + margin-bottom: 8px; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.fl-ai-summary .fl-ai-text { + font-size: 1rem; + color: #1e3a8a; + line-height: 1.7; +} +.fl-ai-summary .fl-ai-disclaimer { + font-size: 0.75rem; + color: #3b82f6; + margin-top: 10px; + font-style: italic; +} + +/* ─── Bilingual toggle ────────────────────────────────────── */ .fl-lang-toggle { float: right; margin-bottom: 10px; } +.fl-lang-toggle .btn { + font-size: 0.82rem; + padding: 3px 10px; +} +[data-lang="es"] { display: none; } +body.fl-lang-es [data-lang="es"] { display: block; } +body.fl-lang-es [data-lang="en"] { display: none; } + +/* ─── Intake form ─────────────────────────────────────────── */ +.fl-intake-step { + display: none; +} +.fl-intake-step.active { + display: block; +} +.fl-intake-progress { + display: flex; + gap: 4px; + margin-bottom: 24px; +} +.fl-intake-progress-step { + flex: 1; + height: 6px; + border-radius: 3px; + background: #dee2e6; +} +.fl-intake-progress-step.completed { + background: #003366; +} +.fl-intake-progress-step.active { + background: #005ca8; +} + +/* ─── Case summary stats (portal home) ───────────────────── */ +.fl-stat-card { + background: #fff; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 16px; + text-align: center; +} +.fl-stat-card .fl-stat-number { + font-size: 2rem; + font-weight: 700; + color: #003366; +} +.fl-stat-card .fl-stat-label { + font-size: 0.82rem; + color: #6c757d; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.fl-stat-card.danger .fl-stat-number { color: #dc3545; } +.fl-stat-card.warning .fl-stat-number { color: #fd7e14; } +.fl-stat-card.success .fl-stat-number { color: #198754; } + +/* ─── Responsive ──────────────────────────────────────────── */ +@media (max-width: 768px) { + .fl-case-header { + padding: 16px; + } + .fl-case-header h2 { + font-size: 1.2rem; + } + .fl-calculator-result { + font-size: 1.2rem; + padding: 16px; + } + .fl-timeline { + padding-left: 20px; + } +} diff --git a/activeblue_familylaw/static/src/js/fl_calculator.js b/activeblue_familylaw/static/src/js/fl_calculator.js index 7643031..eb5538a 100644 --- a/activeblue_familylaw/static/src/js/fl_calculator.js +++ b/activeblue_familylaw/static/src/js/fl_calculator.js @@ -1,9 +1,361 @@ /** @odoo-module **/ /** - * ActiveBlue Family Law — FL 61.30 Interactive Calculator Widget - * Phase 6: Full interactive implementation - * Phase 1: Stub + * ActiveBlue Family Law — FL 61.30 Interactive Child Support Calculator + * Phase 6: Full interactive implementation. + * + * Features: + * - FL 61.30 income-shares calculation (schedule lookup + above-schedule formula) + * - FL 61.30(11)(b) substantial timesharing credit (>73 overnights) + * - Health insurance, childcare, extraordinary expense adjustments + * - Pay frequency converter (weekly/biweekly/semi-monthly/monthly) + * - Bilingual output (EN/ES) + * - Live calculation as user types */ -// Placeholder — full FL 61.30 interactive widget implemented in Phase 6 -console.log('[ActiveBlue FamilyLaw] Calculator widget loaded (Phase 1 stub)'); +import { Component, useState, onMounted } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +// ───────────────────────────────────────────────────────────────────────────── +// FL 61.30 Support Schedule (2024 — Combined Net Monthly Income vs. children) +// Values in dollars per month. Source: FL DOR schedule (verify annually). +// ───────────────────────────────────────────────────────────────────────────── +const FL_SUPPORT_SCHEDULE = [ + // [income_min, income_max, [1child, 2children, 3children, 4children, 5children, 6children]] + [0, 800, [74, 147, 177, 207, 228, 249]], + [801, 850, [153, 261, 319, 374, 411, 449]], + [851, 900, [162, 276, 338, 397, 437, 477]], + [901, 950, [171, 291, 356, 418, 460, 502]], + [951, 1000, [179, 305, 374, 439, 483, 527]], + [1001, 1050, [188, 320, 392, 461, 507, 553]], + [1051, 1100, [196, 334, 409, 481, 529, 578]], + [1101, 1150, [205, 349, 428, 503, 553, 604]], + [1151, 1200, [213, 363, 445, 523, 575, 628]], + [1201, 1250, [221, 376, 461, 542, 596, 651]], + [1251, 1300, [229, 390, 478, 561, 617, 674]], + [1301, 1400, [241, 411, 503, 592, 651, 711]], + [1401, 1500, [257, 438, 537, 631, 694, 758]], + [1501, 1600, [272, 464, 568, 668, 735, 803]], + [1601, 1750, [292, 498, 610, 717, 789, 862]], + [1751, 1900, [316, 539, 660, 776, 854, 932]], + [1901, 2000, [332, 566, 693, 815, 896, 979]], + [2001, 2100, [347, 592, 725, 852, 937, 1023]], + [2101, 2200, [362, 617, 756, 889, 978, 1067]], + [2201, 2300, [377, 643, 787, 926, 1018, 1111]], + [2301, 2400, [392, 669, 819, 963, 1059, 1156]], + [2401, 2500, [407, 694, 850, 1000, 1100, 1200]], + [2501, 2600, [422, 720, 881, 1036, 1139, 1244]], + [2601, 2800, [444, 757, 928, 1091, 1200, 1310]], + [2801, 3000, [467, 797, 976, 1148, 1262, 1378]], + [3001, 3200, [490, 836, 1024, 1204, 1324, 1445]], + [3201, 3400, [513, 875, 1072, 1261, 1386, 1513]], + [3401, 3600, [536, 914, 1120, 1317, 1448, 1580]], + [3601, 3800, [558, 952, 1167, 1373, 1509, 1647]], + [3801, 4000, [580, 990, 1213, 1428, 1570, 1714]], + [4001, 4200, [602, 1028, 1259, 1482, 1629, 1779]], + [4201, 4400, [624, 1065, 1305, 1535, 1688, 1843]], + [4401, 4600, [646, 1102, 1350, 1589, 1747, 1908]], + [4601, 4800, [668, 1140, 1396, 1642, 1806, 1972]], + [4801, 5000, [690, 1177, 1441, 1696, 1865, 2036]], + [5001, 5200, [712, 1214, 1487, 1750, 1924, 2101]], + [5201, 5400, [734, 1252, 1532, 1803, 1983, 2165]], + [5401, 5600, [756, 1289, 1577, 1857, 2042, 2229]], + [5601, 5800, [778, 1326, 1623, 1910, 2101, 2294]], + [5801, 6000, [800, 1364, 1669, 1964, 2160, 2358]], + [6001, 6200, [818, 1396, 1710, 2012, 2213, 2416]], + [6201, 6400, [836, 1426, 1748, 2057, 2263, 2470]], + [6401, 6600, [853, 1456, 1784, 2099, 2309, 2521]], + [6601, 6800, [869, 1483, 1817, 2138, 2351, 2568]], + [6801, 7000, [885, 1510, 1851, 2179, 2396, 2615]], + [7001, 7200, [901, 1538, 1885, 2218, 2440, 2664]], + [7201, 7400, [917, 1565, 1918, 2258, 2484, 2712]], + [7401, 7600, [933, 1592, 1951, 2297, 2527, 2760]], + [7601, 7800, [949, 1619, 1983, 2335, 2569, 2805]], + [7801, 8000, [965, 1647, 2016, 2374, 2612, 2852]], + [8001, 8200, [981, 1674, 2051, 2414, 2655, 2900]], + [8201, 8400, [997, 1702, 2085, 2453, 2698, 2947]], + [8401, 8600, [1013, 1729, 2118, 2492, 2740, 2993]], + [8601, 8800, [1028, 1756, 2151, 2531, 2782, 3039]], + [8801, 9000, [1044, 1783, 2185, 2571, 2827, 3086]], + [9001, 9200, [1060, 1810, 2218, 2610, 2871, 3135]], + [9201, 9400, [1076, 1838, 2252, 2650, 2916, 3183]], + [9401, 9600, [1092, 1865, 2285, 2689, 2958, 3230]], + [9601, 9800, [1107, 1892, 2318, 2728, 3001, 3276]], + [9801, 10000, [1123, 1919, 2351, 2767, 3044, 3321]], +]; + +// Above-schedule percentages per child count (FL 61.30(6)) +const ABOVE_SCHEDULE_PCT = [0.05, 0.075, 0.095, 0.11, 0.12, 0.125]; + +/** + * Look up the basic child support obligation from the FL schedule. + * @param {number} combinedNetIncome - Combined monthly net income + * @param {number} numChildren - Number of children (1-6) + * @returns {number} Basic support obligation in dollars + */ +function getBasicObligation(combinedNetIncome, numChildren) { + const childIdx = Math.min(Math.max(numChildren, 1), 6) - 1; + + if (combinedNetIncome >= 10000) { + // Above-schedule formula: base (10,000 row) + percentage of excess + const basePct = ABOVE_SCHEDULE_PCT[childIdx]; + const base10k = FL_SUPPORT_SCHEDULE[FL_SUPPORT_SCHEDULE.length - 1][2][childIdx]; + const excess = combinedNetIncome - 10000; + return base10k + excess * basePct; + } + + for (const [min, max, amounts] of FL_SUPPORT_SCHEDULE) { + if (combinedNetIncome >= min && combinedNetIncome <= max) { + return amounts[childIdx]; + } + } + return 0; +} + +/** + * FL 61.30 child support calculation. + * @param {Object} inputs + * @returns {Object} Detailed calculation result + */ +function calculateSupport(inputs) { + const { + petitionerNetIncome, + respondentNetIncome, + numChildren, + petitionerHealthInsurance, + respondentHealthInsurance, + petitionerChildcare, + respondentChildcare, + extraordinaryExpenses, + petitionerOvernights, + respondentOvernights, + } = inputs; + + const combined = petitionerNetIncome + respondentNetIncome; + if (combined <= 0 || numChildren <= 0) { + return { error: 'Please enter valid income and number of children.' }; + } + + const petitionerPct = petitionerNetIncome / combined; + const respondentPct = respondentNetIncome / combined; + + // Step 2: Basic obligation + const basicObligation = getBasicObligation(combined, numChildren); + + // Step 3: Adjusted obligation (add health insurance + childcare) + const totalHealthInsurance = petitionerHealthInsurance + respondentHealthInsurance; + const totalChildcare = petitionerChildcare + respondentChildcare; + const adjustedObligation = basicObligation + totalHealthInsurance + totalChildcare + (extraordinaryExpenses || 0); + + // Step 4: Each party's share + const petitionerShare = adjustedObligation * petitionerPct; + const respondentShare = adjustedObligation * respondentPct; + + // Step 4b: Deduct what each party directly pays + const petitionerDirectPay = petitionerHealthInsurance + petitionerChildcare; + const respondentDirectPay = respondentHealthInsurance + respondentChildcare; + let petitionerObligation = petitionerShare - petitionerDirectPay; + let respondentObligation = respondentShare - respondentDirectPay; + + // Step 5: Substantial timesharing (FL 61.30(11)(b)) — if either parent >73 overnights + const totalOvernights = petitionerOvernights + respondentOvernights; + const effectiveTotal = totalOvernights > 0 ? totalOvernights : 365; + const petOvernightPct = petitionerOvernights / effectiveTotal; + const respOvernightPct = respondentOvernights / effectiveTotal; + const substantialTimesharing = petitionerOvernights > 73 || respondentOvernights > 73; + + let timesharingAdjustment = 0; + if (substantialTimesharing) { + // FL 61.30(11)(b) formula: + // Each parent's obligation = adjustedObligation × (1 + other's income %) × (their overnight %) × 1.5 + const petComputed = adjustedObligation * (1 + respondentPct) * petOvernightPct * 1.5; + const respComputed = adjustedObligation * (1 + petitionerPct) * respOvernightPct * 1.5; + // Net: respondent pays petitioner the difference + const netAmount = respComputed - petComputed; + timesharingAdjustment = Math.max(0, netAmount); + respondentObligation = netAmount; + petitionerObligation = -netAmount; // petitioner receives + } + + // Who pays? + const respondentPays = respondentObligation > petitionerObligation; + const netPayment = Math.abs(respondentPays ? respondentObligation : petitionerObligation); + + return { + combined, + petitionerPct: Math.round(petitionerPct * 1000) / 10, + respondentPct: Math.round(respondentPct * 1000) / 10, + basicObligation: Math.round(basicObligation * 100) / 100, + adjustedObligation: Math.round(adjustedObligation * 100) / 100, + petitionerObligation: Math.round(petitionerObligation * 100) / 100, + respondentObligation: Math.round(respondentObligation * 100) / 100, + substantialTimesharing, + timesharingAdjustment: Math.round(timesharingAdjustment * 100) / 100, + netPayment: Math.round(netPayment * 100) / 100, + respondentPays, + aboveSchedule: combined > 10000, + }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Portal calculator widget (Owl component — attaches to .fl-calculator-widget) +// ───────────────────────────────────────────────────────────────────────────── + +function formatMoney(n) { + return '$' + Math.abs(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function initPortalCalculator() { + const container = document.querySelector('.fl-calculator-widget'); + if (!container) return; + + const state = { + petitionerNetIncome: 0, + respondentNetIncome: 0, + numChildren: 1, + petitionerHealthInsurance: 0, + respondentHealthInsurance: 0, + petitionerChildcare: 0, + respondentChildcare: 0, + extraordinaryExpenses: 0, + petitionerOvernights: 182, + respondentOvernights: 183, + }; + + function getVal(id) { + const el = document.getElementById(id); + return el ? (parseFloat(el.value) || 0) : 0; + } + + function recalculate() { + Object.keys(state).forEach(k => { + if (k !== 'numChildren') { + state[k] = getVal(k); + } + }); + const nc = document.getElementById('numChildren'); + state.numChildren = nc ? (parseInt(nc.value) || 1) : 1; + + const result = calculateSupport(state); + const resultDiv = document.querySelector('.fl-calculator-result-area'); + const breakdownDiv = document.querySelector('.fl-calculator-breakdown'); + + if (result.error) { + if (resultDiv) resultDiv.innerHTML = `
${result.error}
`; + return; + } + + const payer = result.respondentPays ? 'Respondent' : 'Petitioner'; + + if (resultDiv) { + resultDiv.innerHTML = ` +
+
NET MONTHLY CHILD SUPPORT
+
${formatMoney(result.netPayment)} / month
+
${payer} pays
+
`; + } + + if (breakdownDiv) { + breakdownDiv.innerHTML = ` + + + + + + + ${result.substantialTimesharing ? `` : ''} + +
Combined Net Monthly Income${formatMoney(result.combined)}
Petitioner share (${result.petitionerPct}%)${formatMoney(result.combined * result.petitionerPct / 100)}
Respondent share (${result.respondentPct}%)${formatMoney(result.combined * result.respondentPct / 100)}
Basic FL 61.30 Obligation${result.aboveSchedule ? ' (above-schedule formula)' : ''}${formatMoney(result.basicObligation)}
Adjusted Obligation (+ ins/childcare)${formatMoney(result.adjustedObligation)}
Substantial Timesharing (FL 61.30(11)(b))Applied
Net Monthly Payment${formatMoney(result.netPayment)}
+
+ This is an estimate only. Official court calculation may differ. + Calculate in system → +
`; + } + } + + // Attach live listeners + container.querySelectorAll('input, select').forEach(el => { + el.addEventListener('input', recalculate); + el.addEventListener('change', recalculate); + }); + + // Initial calculation + recalculate(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Language toggle +// ───────────────────────────────────────────────────────────────────────────── +function initLangToggle() { + document.querySelectorAll('.fl-lang-btn').forEach(btn => { + btn.addEventListener('click', () => { + const lang = btn.dataset.lang; + document.body.classList.toggle('fl-lang-es', lang === 'es'); + document.body.classList.toggle('fl-lang-en', lang === 'en'); + document.querySelectorAll('.fl-lang-btn').forEach(b => b.classList.remove('btn-primary')); + btn.classList.add('btn-primary'); + localStorage.setItem('fl_lang', lang); + }); + }); + // Restore preference + const saved = localStorage.getItem('fl_lang'); + if (saved) { + const btn = document.querySelector(`.fl-lang-btn[data-lang="${saved}"]`); + if (btn) btn.click(); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Multi-step intake form +// ───────────────────────────────────────────────────────────────────────────── +function initIntakeWizard() { + const form = document.querySelector('.fl-intake-form'); + if (!form) return; + + let currentStep = 0; + const steps = form.querySelectorAll('.fl-intake-step'); + const progressSteps = form.querySelectorAll('.fl-intake-progress-step'); + const stepCounter = form.querySelector('.fl-step-counter'); + + function showStep(n) { + steps.forEach((s, i) => { + s.classList.toggle('active', i === n); + }); + progressSteps.forEach((s, i) => { + s.classList.remove('active', 'completed'); + if (i < n) s.classList.add('completed'); + if (i === n) s.classList.add('active'); + }); + if (stepCounter) { + stepCounter.textContent = `Step ${n + 1} of ${steps.length}`; + } + currentStep = n; + window.scrollTo({ top: form.offsetTop - 20, behavior: 'smooth' }); + } + + form.querySelectorAll('.fl-next-btn').forEach(btn => { + btn.addEventListener('click', () => { + if (currentStep < steps.length - 1) showStep(currentStep + 1); + }); + }); + form.querySelectorAll('.fl-prev-btn').forEach(btn => { + btn.addEventListener('click', () => { + if (currentStep > 0) showStep(currentStep - 1); + }); + }); + + showStep(0); +} + +// ───────────────────────────────────────────────────────────────────────────── +// DOMContentLoaded — initialize all widgets +// ───────────────────────────────────────────────────────────────────────────── +document.addEventListener('DOMContentLoaded', () => { + initPortalCalculator(); + initLangToggle(); + initIntakeWizard(); + console.log('[ActiveBlue FamilyLaw] Portal widgets initialized'); +}); + +// Export for OWL/module use +export { calculateSupport, getBasicObligation, formatMoney }; diff --git a/activeblue_familylaw/static/src/js/fl_timeline.js b/activeblue_familylaw/static/src/js/fl_timeline.js index 87fe191..c961308 100644 --- a/activeblue_familylaw/static/src/js/fl_timeline.js +++ b/activeblue_familylaw/static/src/js/fl_timeline.js @@ -1,9 +1,152 @@ /** @odoo-module **/ /** - * ActiveBlue Family Law — Visual Timeline Widget - * Phase 6: Full visual timeline - * Phase 1: Stub + * ActiveBlue Family Law — Case Timeline Widget + * Phase 6: Interactive deadline/event timeline for the portal. + * + * Features: + * - Reads deadline data from embedded JSON script tag (#fl-timeline-data) + * - Color-codes by urgency (overdue / urgent / soon / ok / completed) + * - Filters: All / Pending / Overdue / Completed + * - Bilingual labels (EN/ES via body.fl-lang-es) + * - Mark-complete via /family-law/deadline/complete AJAX endpoint */ -// Placeholder — visual deadline timeline implemented in Phase 6 -console.log('[ActiveBlue FamilyLaw] Timeline widget loaded (Phase 1 stub)'); +'use strict'; + +function daysBetween(dateStr) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const d = new Date(dateStr); + d.setHours(0, 0, 0, 0); + return Math.round((d - today) / 86400000); +} + +function urgencyClass(daysUntil, completed) { + if (completed) return 'completed'; + if (daysUntil < 0) return 'overdue'; + if (daysUntil <= 7) return 'urgent'; + if (daysUntil <= 14) return 'soon'; + return 'ok'; +} + +function urgencyLabel(daysUntil, completed) { + if (completed) return { en: 'Completed', es: 'Completado' }; + if (daysUntil < 0) return { en: `${Math.abs(daysUntil)} days overdue`, es: `${Math.abs(daysUntil)} días vencido` }; + if (daysUntil === 0) return { en: 'Due TODAY', es: 'Vence HOY' }; + if (daysUntil === 1) return { en: 'Due tomorrow', es: 'Vence mañana' }; + return { en: `Due in ${daysUntil} days`, es: `Vence en ${daysUntil} días` }; +} + +function escapeHtml(str) { + return String(str || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +function renderTimeline(container, deadlines, filter, lang) { + const list = container.querySelector('.fl-timeline-list'); + if (!list) return; + + const filtered = deadlines.filter(d => { + if (filter === 'pending') return !d.completed; + if (filter === 'completed') return d.completed; + if (filter === 'overdue') return !d.completed && daysBetween(d.dueDate) < 0; + return true; + }); + + filtered.sort((a, b) => { + if (a.completed && !b.completed) return 1; + if (!a.completed && b.completed) return -1; + return new Date(a.dueDate) - new Date(b.dueDate); + }); + + const isEs = lang === 'es'; + if (filtered.length === 0) { + list.innerHTML = `

${isEs ? 'No hay fechas límite para mostrar.' : 'No deadlines to display.'}

`; + return; + } + + list.innerHTML = filtered.map(d => { + const days = daysBetween(d.dueDate); + const uClass = urgencyClass(days, d.completed); + const uLabel = urgencyLabel(days, d.completed); + const labelText = isEs ? uLabel.es : uLabel.en; + const nameText = isEs ? (d.nameEs || d.name) : d.name; + const dot = uClass === 'overdue' ? 'overdue' : uClass === 'completed' ? 'completed' : ''; + + return `
+
${new Date(d.dueDate).toLocaleDateString('en-US', {year:'numeric',month:'short',day:'numeric'})}
+
${escapeHtml(nameText)}
+
+ ${escapeHtml(labelText)} + ${d.statRef ? ` — ${escapeHtml(d.statRef)}` : ''} +
+ ${!d.completed ? `` : ''} +
`; + }).join(''); +} + +function initTimeline() { + const container = document.querySelector('.fl-timeline-widget'); + if (!container) return; + + const dataEl = document.getElementById('fl-timeline-data'); + let deadlines = []; + if (dataEl) { + try { deadlines = JSON.parse(dataEl.textContent || '[]'); } + catch (e) { console.warn('[FLTimeline] Could not parse timeline data', e); } + } + + let currentFilter = 'pending'; + let currentLang = document.body.classList.contains('fl-lang-es') ? 'es' : 'en'; + + renderTimeline(container, deadlines, currentFilter, currentLang); + + container.querySelectorAll('.fl-timeline-filter').forEach(btn => { + btn.addEventListener('click', () => { + container.querySelectorAll('.fl-timeline-filter').forEach(b => b.classList.remove('active', 'btn-primary')); + btn.classList.add('active', 'btn-primary'); + currentFilter = btn.dataset.filter; + renderTimeline(container, deadlines, currentFilter, currentLang); + }); + }); + + // Listen for language changes dispatched by fl_calculator.js + document.body.addEventListener('fl-lang-change', (e) => { + currentLang = e.detail && e.detail.lang || 'en'; + renderTimeline(container, deadlines, currentFilter, currentLang); + }); + + // Mark complete via AJAX + container.addEventListener('click', (e) => { + const btn = e.target.closest('.fl-mark-complete'); + if (!btn) return; + const deadlineId = parseInt(btn.dataset.id); + if (!deadlineId) return; + btn.disabled = true; + btn.textContent = '...'; + + fetch('/family-law/deadline/complete', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, + body: JSON.stringify({ deadline_id: deadlineId }), + }) + .then(r => r.json()) + .then(data => { + if (data.success) { + const dl = deadlines.find(d => d.id === deadlineId); + if (dl) dl.completed = true; + renderTimeline(container, deadlines, currentFilter, currentLang); + } else { + btn.disabled = false; + btn.textContent = currentLang === 'es' ? 'Marcar completo' : 'Mark complete'; + } + }) + .catch(() => { + btn.disabled = false; + btn.textContent = currentLang === 'es' ? 'Marcar completo' : 'Mark complete'; + }); + }); +} + +document.addEventListener('DOMContentLoaded', initTimeline); + +export { initTimeline, renderTimeline, daysBetween, urgencyClass }; diff --git a/activeblue_familylaw/views/portal_calculator_templates.xml b/activeblue_familylaw/views/portal_calculator_templates.xml new file mode 100644 index 0000000..11b504f --- /dev/null +++ b/activeblue_familylaw/views/portal_calculator_templates.xml @@ -0,0 +1,211 @@ + + + + + + + + + diff --git a/activeblue_familylaw/views/portal_case_templates.xml b/activeblue_familylaw/views/portal_case_templates.xml new file mode 100644 index 0000000..1d252fb --- /dev/null +++ b/activeblue_familylaw/views/portal_case_templates.xml @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + diff --git a/activeblue_familylaw/views/portal_caselaw_templates.xml b/activeblue_familylaw/views/portal_caselaw_templates.xml new file mode 100644 index 0000000..51c409c --- /dev/null +++ b/activeblue_familylaw/views/portal_caselaw_templates.xml @@ -0,0 +1,201 @@ + + + + + + + + + diff --git a/activeblue_familylaw/views/website_intake_templates.xml b/activeblue_familylaw/views/website_intake_templates.xml new file mode 100644 index 0000000..82ea25c --- /dev/null +++ b/activeblue_familylaw/views/website_intake_templates.xml @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + +